From d940723ab0339f2c78748a5ea56142b79eb24344 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 3 Oct 2024 13:51:43 +0100 Subject: [PATCH 01/97] Added: oidc-pkce boilerplate --- dev-env/docker-compose-dev.yml | 23 + dev-env/keycloak/test-realm.json | 2217 ++++++++++++++++++++++++++++++ dev-env/nginx.conf | 25 + package-lock.json | 9 + package.json | 1 + src/SecuredApp.tsx | 24 + src/index.tsx | 4 +- 7 files changed, 2301 insertions(+), 2 deletions(-) create mode 100644 dev-env/keycloak/test-realm.json create mode 100644 src/SecuredApp.tsx diff --git a/dev-env/docker-compose-dev.yml b/dev-env/docker-compose-dev.yml index cdefa5ea0..db3b1888e 100644 --- a/dev-env/docker-compose-dev.yml +++ b/dev-env/docker-compose-dev.yml @@ -9,6 +9,7 @@ services: depends_on: - dev_dataverse - dev_frontend + - dev_keycloak volumes: - ./nginx.conf:/etc/nginx/nginx.conf - ./docker-dev-volumes/nginx/logs:/var/log/nginx/ @@ -172,6 +173,28 @@ services: tmpfs: - /mail:mode=770,size=128M,uid=1000,gid=1000 + dev_keycloak: + container_name: 'dev_keycloak' + image: 'quay.io/keycloak/keycloak:21.0' + hostname: keycloak + command: + - 'start-dev' + - '--import-realm' + environment: + - KC_HTTP_PORT=9080 + - KC_HOSTNAME=localhost + - KC_HOSTNAME_PORT=8000 + - KC_HOSTNAME_ADMIN_URL=http://localhost:8000 + - KEYCLOAK_ADMIN=kcadmin + - KEYCLOAK_ADMIN_PASSWORD=kcpassword + - KEYCLOAK_LOGLEVEL=DEBUG + networks: + - dataverse + expose: + - 9080 + volumes: + - './keycloak/test-realm.json:/opt/keycloak/data/import/test-realm.json' + networks: dataverse: driver: bridge diff --git a/dev-env/keycloak/test-realm.json b/dev-env/keycloak/test-realm.json new file mode 100644 index 000000000..ba6721d80 --- /dev/null +++ b/dev-env/keycloak/test-realm.json @@ -0,0 +1,2217 @@ +{ + "id": "80a7e04b-a2b5-4891-a2d1-5ad4e915f983", + "realm": "test", + "displayName": "", + "displayNameHtml": "", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "none", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "075daee1-5ab2-44b5-adbf-fa49a3da8305", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "80a7e04b-a2b5-4891-a2d1-5ad4e915f983", + "attributes": {} + }, + { + "id": "b4ff9091-ddf9-4536-b175-8cfa3e331d71", + "name": "default-roles-test", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": ["offline_access", "uma_authorization"], + "client": { + "account": ["view-profile", "manage-account"] + } + }, + "clientRole": false, + "containerId": "80a7e04b-a2b5-4891-a2d1-5ad4e915f983", + "attributes": {} + }, + { + "id": "e6d31555-6be6-4dee-bc6a-40a53108e4c2", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "80a7e04b-a2b5-4891-a2d1-5ad4e915f983", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "1955bd12-5f86-4a74-b130-d68a8ef6f0ee", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "1109c350-9ab1-426c-9876-ef67d4310f35", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "980c3fd3-1ae3-4b8f-9a00-d764c939035f", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "5363e601-0f9d-4633-a8c8-28cb0f859b7b", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "59aa7992-ad78-48db-868a-25d6e1d7db50", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "impersonation", + "view-authorization", + "query-users", + "query-groups", + "manage-clients", + "manage-realm", + "view-identity-providers", + "query-realms", + "manage-authorization", + "manage-identity-providers", + "manage-users", + "view-users", + "view-realm", + "create-client", + "view-clients", + "manage-events", + "query-clients", + "view-events" + ] + } + }, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "112f53c2-897d-4c01-81db-b8dc10c5b995", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "c7f57bbd-ef32-4a64-9888-7b8abd90777a", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "8885dac8-0af3-45af-94ce-eff5e801bb80", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "2673346c-b0ef-4e01-8a90-be03866093af", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "b7182885-9e57-445f-8dae-17c16eb31b5d", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "ba7bfe0c-cb07-4a47-b92c-b8132b57e181", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "13a8f0fc-647d-4bfe-b525-73956898e550", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "ef4c57dc-78c2-4f9a-8d2b-0e97d46fc842", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "2875da34-006c-4b7f-bfc8-9ae8e46af3a2", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": ["query-users", "query-groups"] + } + }, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "c8c8f7dc-876b-4263-806f-3329f7cd5fd3", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "21b84f90-5a9a-4845-a7ba-bbd98ac0fcc4", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": ["query-clients"] + } + }, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "6fd64c94-d663-4501-ad77-0dcf8887d434", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "b321927a-023c-4d2a-99ad-24baf7ff6d83", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + }, + { + "id": "2fc21160-78de-457b-8594-e5c76cde1d5e", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "dada0ae8-ee9f-415a-9685-42da7c563660", + "attributes": {} + } + ], + "test": [], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "07ee59b5-dca6-48fb-83d4-2994ef02850e", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "b57d62bb-77ff-42bd-b8ff-381c7288f327", + "attributes": {} + } + ], + "account": [ + { + "id": "17d2f811-7bdf-4c73-83b4-1037001797b8", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "77f8127a-261e-4cd8-a77d-b74a389f7fd4", + "attributes": {} + }, + { + "id": "d1ff44f9-419e-42fd-98e8-1add1169a972", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "77f8127a-261e-4cd8-a77d-b74a389f7fd4", + "attributes": {} + }, + { + "id": "14c23a18-ae2d-43c9-b0c0-aaf6e0c7f5b0", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "77f8127a-261e-4cd8-a77d-b74a389f7fd4", + "attributes": {} + }, + { + "id": "6fbe58af-d2fe-4d66-95fe-a2e8a818cb55", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "77f8127a-261e-4cd8-a77d-b74a389f7fd4", + "attributes": {} + }, + { + "id": "bdfd02bc-6f6a-47d2-82bc-0ca52d78ff48", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": ["view-consent"] + } + }, + "clientRole": true, + "containerId": "77f8127a-261e-4cd8-a77d-b74a389f7fd4", + "attributes": {} + }, + { + "id": "782f3b0c-a17b-4a87-988b-1a711401f3b0", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": ["manage-account-links"] + } + }, + "clientRole": true, + "containerId": "77f8127a-261e-4cd8-a77d-b74a389f7fd4", + "attributes": {} + }, + { + "id": "8a3bfe15-66d9-4f3d-83ac-801d682d42b0", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "77f8127a-261e-4cd8-a77d-b74a389f7fd4", + "attributes": {} + } + ] + } + }, + "groups": [ + { + "id": "d46f94c2-3b47-4288-b937-9cf918e54f0a", + "name": "admins", + "path": "/admins", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [] + }, + { + "id": "e992ce15-baac-48a0-8834-06f6fcf6c05b", + "name": "curators", + "path": "/curators", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [] + }, + { + "id": "531cf81d-a700-4336-808f-37a49709b48c", + "name": "members", + "path": "/members", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [] + } + ], + "defaultRole": { + "id": "b4ff9091-ddf9-4536-b175-8cfa3e331d71", + "name": "default-roles-test", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "80a7e04b-a2b5-4891-a2d1-5ad4e915f983" + }, + "requiredCredentials": ["password"], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": ["FreeOTP", "Google Authenticator"], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": ["ES256"], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "users": [ + { + "id": "52cddd46-251c-4534-acc8-0580eeafb577", + "createdTimestamp": 1684736014759, + "username": "admin", + "enabled": true, + "totp": false, + "emailVerified": true, + "firstName": "Dataverse", + "lastName": "Admin", + "email": "dataverse-admin@mailinator.com", + "credentials": [ + { + "id": "28f1ece7-26fb-40f1-9174-5ffce7b85c0a", + "type": "password", + "userLabel": "Set to \"admin\"", + "createdDate": 1684736057302, + "secretData": "{\"value\":\"ONI7fl6BmooVTUgwN1W3m7hsRjMAYEr2l+Fp5+7IOYw1iIntwvZ3U3W0ZBcCFJ7uhcKqF101+rueM3dZfoshPQ==\",\"salt\":\"Hj7co7zYVei7xwx8EaYP3A==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-test"], + "notBefore": 0, + "groups": ["/admins"] + }, + { + "id": "a3d8e76d-7e7b-42dc-bbd7-4258818a8a1b", + "createdTimestamp": 1684755806552, + "username": "affiliate", + "enabled": true, + "totp": false, + "emailVerified": true, + "firstName": "Dataverse", + "lastName": "Affiliate", + "email": "dataverse-affiliate@mailinator.com", + "credentials": [ + { + "id": "31c8eb1e-b2a8-4f86-833b-7c0536cd61a1", + "type": "password", + "userLabel": "My password", + "createdDate": 1684755821743, + "secretData": "{\"value\":\"T+RQ4nvmjknj7ds8NU7782j6PJ++uCu98zNoDQjIe9IKXah+13q4EcXO9IHmi2BJ7lgT0OIzwIoac4JEQLxhjQ==\",\"salt\":\"fnRmE9WmjAp4tlvGh/bxxQ==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-test"], + "notBefore": 0, + "groups": [] + }, + { + "id": "e5531496-cfb8-498c-a902-50c98d649e79", + "createdTimestamp": 1684755721064, + "username": "curator", + "enabled": true, + "totp": false, + "emailVerified": true, + "firstName": "Dataverse", + "lastName": "Curator", + "email": "dataverse-curator@mailinator.com", + "credentials": [ + { + "id": "664546b4-b936-45cf-a4cf-5e98b743fc7f", + "type": "password", + "userLabel": "My password", + "createdDate": 1684755740776, + "secretData": "{\"value\":\"AvVqybCNtCBVAdLEeJKresy9tc3c4BBUQvu5uHVQw4IjVagN6FpKGlDEKOrxhzdSM8skEvthOEqJkloPo1w+NQ==\",\"salt\":\"2em2DDRRlNEYsNR3xDqehw==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-test"], + "notBefore": 0, + "groups": ["/curators"] + }, + { + "id": "c0082e7e-a3e9-45e6-95e9-811a34adce9d", + "createdTimestamp": 1684755585802, + "username": "user", + "enabled": true, + "totp": false, + "emailVerified": true, + "firstName": "Dataverse", + "lastName": "User", + "email": "dataverse-user@mailinator.com", + "credentials": [ + { + "id": "00d6d67f-2e30-4da6-a567-bec38a1886a0", + "type": "password", + "userLabel": "My password", + "createdDate": 1684755599597, + "secretData": "{\"value\":\"z991rnjznAgosi5nX962HjM8/gN5GLJTdrlvi6G9cj8470X2/oZUb4Lka6s8xImgtEloCgWiKqH0EH9G4Y3a5A==\",\"salt\":\"/Uz7w+2IqDo+fQUGqxjVHw==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-test"], + "notBefore": 0, + "groups": ["/members"] + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": ["offline_access"] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": ["manage-account"] + } + ] + }, + "clients": [ + { + "id": "77f8127a-261e-4cd8-a77d-b74a389f7fd4", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/test/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["/realms/test/account/*"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + }, + { + "id": "5d99f721-027c-478d-867d-61114e0a8192", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/test/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["/realms/test/account/*"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "e181a0ce-9a04-4468-a38a-aaef9f78f989", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + }, + { + "id": "5eccc178-121e-4d0f-bcb2-04ae3c2e52ed", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + }, + { + "id": "b57d62bb-77ff-42bd-b8ff-381c7288f327", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + }, + { + "id": "dada0ae8-ee9f-415a-9685-42da7c563660", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + }, + { + "id": "bf7cf550-3875-4f97-9878-b2419a854058", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/test/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["/admin/test/console/*"], + "webOrigins": ["+"], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "ff845e16-e200-4894-ab51-37d8b9f2a445", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + }, + { + "id": "9c27faa8-4b8d-4ad9-9cd1-880032ef06aa", + "clientId": "test", + "name": "A Test Client", + "description": "Use for hacking and testing away a confidential client", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8", + "redirectUris": ["*"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1684735831", + "backchannel.logout.session.required": "true", + "display.on.consent.screen": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + } + ], + "clientScopes": [ + { + "id": "72f29e57-92fa-437b-828c-2b9d6fe56192", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "59581aea-70d6-4ee8-bec2-1fea5fc497ae", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "f515ec81-3c1b-4d4d-b7a2-e7e8d47b6447", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "26d299a8-69e2-4864-9595-17a5b417fc61", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "d2998083-a8db-4f4e-9aaa-9cad68d65b97", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "7a4cb2e5-07a0-4c16-a024-71df7ddd6868", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "8f1eafef-92d6-434e-b9ec-6edec1fddd0a", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "c03095aa-b656-447a-9767-0763c2ccb070", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "948b230c-56d0-4000-937c-841cd395d3f9", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "cdf35f63-8ec7-41a0-ae12-f05d415818cc", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "ba4348ff-90b1-4e09-89a8-e5c08b04d3d1", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "e6cceae5-8392-4348-b302-f610ece6056e", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "4318001c-2970-41d3-91b9-e31c08569872", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "406d02a6-866a-4962-8838-e8c58ada1505", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "33baabc1-9bf2-42e4-8b8e-a53c13f0b744", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "5277a84f-d727-4c64-8432-d513127beee1", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "0a609875-2678-4056-93ef-dd5c03e6059d", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "7c510d18-07ee-4b78-8acd-24b777d11b3c", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "0bb6d0ea-195f-49e8-918c-c419a26a661c", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "5f1e644c-1acf-440c-b1a6-b5f65bcebfd9", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "c710bdb2-6cfd-4f60-9c4e-730188fc62f7", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "012d5038-0e13-42ba-9df7-2487c8e2eead", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "21590b19-517d-4b6d-92f6-d4f71238677e", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "e4cddca7-1360-42f3-9854-da6cbe00c71e", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "afee328f-c64c-43e6-80d0-be2721c2ed0e", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "780a1e2c-5b63-46f4-a5bf-dc3fd8ce0cbb", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "aeebffff-f776-427e-83ed-064707ffce57", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "b3e840a2-1794-4da1-bf69-31905cbff0d6", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "0607e0e4-4f7f-4214-996d-3599772ce1c7", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "426a609b-4e28-4132-af0d-13297b8cb63a", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "a1ebde82-ce21-438f-a3ad-261d3eeb1c01", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "64653ac7-7ffc-4f7c-a589-03e3b68bbd25", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "aeb5b852-dfec-4e67-9d9e-104abe9b3bf2", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "e2fa8437-a0f1-46fc-af9c-c40fc09cd6a1", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "4fecd0d7-d4ad-457e-90f2-c7202bf01ff5", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "a9536634-a9f6-4ed5-a8e7-8379d3b002ca", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "2ce1a702-9458-4926-9b8a-f82c07215755", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + } + ], + "defaultDefaultClientScopes": ["role_list", "profile", "email", "roles", "web-origins", "acr"], + "defaultOptionalClientScopes": ["offline_access", "address", "phone", "microprofile-jwt"], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": ["jboss-logging"], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "8115796f-8f1f-4d6a-88f8-ca2938451260", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": ["true"] + } + }, + { + "id": "044bd055-714d-478e-aa93-303d2161c427", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-property-mapper", + "oidc-address-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-role-list-mapper", + "saml-user-attribute-mapper", + "oidc-usermodel-property-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-full-name-mapper" + ] + } + }, + { + "id": "be465734-3b0f-4370-a144-73db756e23f8", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-attribute-mapper", + "saml-user-property-mapper", + "oidc-address-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-attribute-mapper", + "oidc-full-name-mapper", + "oidc-usermodel-property-mapper", + "saml-role-list-mapper" + ] + } + }, + { + "id": "42a2f64d-ac9e-4221-9cf6-40ff8c868629", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": ["true"], + "client-uris-must-match": ["true"] + } + }, + { + "id": "7ca08915-6c33-454c-88f2-20e1d6553b26", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": ["200"] + } + }, + { + "id": "f01f2b6f-3f01-4d01-b2f4-70577c6f599c", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": ["true"] + } + }, + { + "id": "516d7f21-f21a-4690-831e-36ad313093b2", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "c79df6a0-d4d8-4866-b9e6-8ddb5d1bd38e", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + } + ], + "org.keycloak.userprofile.UserProfileProvider": [ + { + "id": "cf47a21f-c8fb-42f2-9bff-feca967db183", + "providerId": "declarative-user-profile", + "subComponents": {}, + "config": {} + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "6b4a2281-a9e8-43ab-aee7-190ae91b2842", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "kid": ["47b9c2c2-32dc-4317-bd8b-1c4e5bb740ca"], + "secret": ["9VWsVSqbj5zWa8Mq-rRzOw"], + "priority": ["100"] + } + }, + { + "id": "68e2d2b0-4976-480f-ab76-f84a17686b05", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEpQIBAAKCAQEAwuIcVVJDncorsQcFef4M/J9dsaNNmwEv/+4pCSZuco7IlA9uCfvwjYgfwQlWoCHCc7JFEtUOXhpLNR0SJ9w2eCC9A/0horjLmiVGU5sGACGrAxSgipt399k83mtkPBTikT1BXumPrX51ovdEPVPQSO0hIBwFn4ZDwA9P/00jNzzswyLC2UDdQrwIjm2xWjq1X82d8mL3+Yp8lF9qD1w305+XPiqCC+TUunKsuCQq5sddet+UoCDsFQyxsJi6cWJrryDvQmiDgM2wm68jn6hyzDE76J1az0wKEGqoMEwIy0juqZCyAqgsm3xA+zHpTcI3EyTwDGpMvWNJp8AWqXPNaQIDAQABAoIBAAethL1+n/6WpUBEaoHcVrq5/2+vo0+dfTyVZNKRFqtG0WOWPzOflFd1HZV7YVPuJI+uPi8ANmsnbh9YcaYg9JiTZ0hMZ++giBf0ID2hZxv995NyXnf7fkoFKghevYG+9mVPtHRmxKlKiPFWfHQjP1ACNKAD2UZdcdbzxicaIkPV/hP996mZA3xaaudggAJq7u/W67H2Q6ofGqW4TI5241d8T+6yobbvXRe4n8FKz4eK2aZv+N+zwh5JDMsJ8050+lCDsyoyakEPf+4veuPkewx4FemAiotDNcmoUQSDL26wLw8kk1uZ9JY0M88OL5pMyBuxTqy0F6BWBltq80mlefECgYEA4vZ8Agu2plXOzWASn0dyhCel3QoeUqNY8D8A+0vK9qWxUE9jMG13jAZmsL2I38SuwRN1DhJezbrn4QTuxTukxgSjLDv/pBp9UnXnCz/fg4yPTYsZ0zHqTMbwvdtfIzBHTCYyIJ+unxVYoenC0XZKSQXA3NN2zNqYpLhjStWdEZECgYEA29DznJxpDZsRUieRxFgZ+eRCjbQ9Q2A46preqMo1KOZ6bt9avxG3uM7pUC+UOeIizeRzxPSJ2SyptYPzdaNwKN3Lq+RhjHe1zYLngXb0CIQaRwNHqePxXF1sg0dTbmcxf+Co7yPG+Nd5nrQq9SQHC3tLTyL6x3VU/yAfMQqUklkCgYEAyVl8iGAV6RkE/4R04OOEv6Ng7WkVn6CUvYZXe5kw9YHnfWUAjS0AOrRPFAsBy+r0UgvN8+7uNjvTjPhQT5/rPVVN4WdVEyQA/E/m6j7/LvhbBaMbBRcqUnTHjNd6XoBtMCxOmkyvoShR2krE8AiuPHwjLoVXxsNDWhbO18wMrVECgYEAlmkICOXNzI2K8Jg62gse2yshjy0BrpSs3XtTWFPkxDPRGwSiZ5OMD10lsMSdvG3MOu5TeTWLDZvOFHJRqPFI0e3Sa7A+P4u6TwF/v8rRePJLuMO5ybo7cWRL2Bh6MlVSPZpQfjIQ+D0Y70uBCXS5jVW0VlYtG0Zh/qDQNxJyTyECgYEAuRINlZ0ag+1QTITapSatbFWd/KquGLpMjZyF4k5gVHs+4zHnnTi1YIDUInp1FJBqKD27z2byy7KFgbMBZQmsDs8i4fgzQrJHe3D4WFFHCjiClbeReejbas9bOnqhSQCiIy1Ck8vMAriAtctSA/g/qq6dQApSgcWaKvTVL2Ywa7E=" + ], + "keyUse": ["ENC"], + "certificate": [ + "MIIClzCCAX8CBgGIQhOIijANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDAR0ZXN0MB4XDTIzMDUyMjA2MDczNloXDTMzMDUyMjA2MDkxNlowDzENMAsGA1UEAwwEdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMLiHFVSQ53KK7EHBXn+DPyfXbGjTZsBL//uKQkmbnKOyJQPbgn78I2IH8EJVqAhwnOyRRLVDl4aSzUdEifcNnggvQP9IaK4y5olRlObBgAhqwMUoIqbd/fZPN5rZDwU4pE9QV7pj61+daL3RD1T0EjtISAcBZ+GQ8APT/9NIzc87MMiwtlA3UK8CI5tsVo6tV/NnfJi9/mKfJRfag9cN9Oflz4qggvk1LpyrLgkKubHXXrflKAg7BUMsbCYunFia68g70Jog4DNsJuvI5+ocswxO+idWs9MChBqqDBMCMtI7qmQsgKoLJt8QPsx6U3CNxMk8AxqTL1jSafAFqlzzWkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAIEIfjqOr2m+8s2RR8VW/nBgOgu9HtPRda4qNhGbgBkZ8NDy7TwHqlHo1ujKW5RO438pRyLJmOibWN4a/rkUsSjin6vgy4l8KpQy+7a4cQCQHyl34TmPjbtiw1jKgiOjzRQY54NVwIJNMIMc1ZyQo4u0U30/FxgUv6akXfS5O1ePD+5xKOOC/Af9AletjhQMPwVxXDwFqfQf/p+SM4Pyn4L633MESfDrH8v9FjJd0lV5ZlEI4hpPtnbi9U+CInqCy3VDNlZjsXswaDRujjg3LERfOMvCgj+Dck3FzWG7EiCwXWNEPvdMzv4w7M6KXuiPPQkST8DUWjgkjUCeLBzT3yw==" + ], + "priority": ["100"], + "algorithm": ["RSA-OAEP"] + } + }, + { + "id": "728769a3-99a4-4cca-959d-28181dfee7e8", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEowIBAAKCAQEAxIszQCv8bX3sKXJVtuLJV6cH/uhkzxcTEIcDe7y2Y2SFM0x2nF6wRLk8QkvIrRmelilegUIJttqZxLXMpxwUJGizehHQMrOCzNoGBZdVanoK7nNa5+FOYtlvL4GxNfwzS36sp3PnKQiGv5Q7RGuPthjLFfqTmYx/7GTDJC4vLEW5S01Vy/Xc9FE4FsT0hnm91lRWjppc9893M5QUy/TPu8udIuNV87Ko5yiIxQqcPiAQXJaN4CyGaDcYhhzzHdxVptIk2FvtxhpmNxrbtmBCx/o9/rBDQNTis8Ex6ItWC2PvC17UPvyOcZ4Fv/qO0L6JZ0mrpH95CeDU1kEP+KKZrwIDAQABAoIBAGGl6SYiVG1PyTQEXqqY/UCjt3jBnEg5ZhrpgWUKKrGyAO2uOSXSc5AJWfN0NHUwC9b+IbplhW8IJ6qQSmfiLu2x6S2mSQLPphZB4gkIGYNntCOpQ0p+aZP6BGAddt5j+VYyTvR5RKlh15S6QEHrkMB/i/LVBl0c7XeUzlEc8wnyj8DGvlmpcQzIcbWfqEZ/FciDdKGNN0M4V/r1uQiOUVZ69SWDBBwu41YwF7PYUsX83q8zn0nBeMqz0ggSf33lW4w31fox9c7EjIF01gPArE5uT+d+AwjVKHpd08LWGR9W9NSXVOPUKkzOM+PyvKGvzjMnlrm/feqowKQbL2q/GP0CgYEA/EsrvUojkFIWxHc19KJdJvqlYgLeWq6P/J7UmHgpl+S3nG6b9HH4/aM/ICDa5hxd5bmP5p2V3EuZWnyb6/QB5eipC7Ss3oM7XeS/PwvTp6NTC1fypx2zHKse3iuLeCGneRxiw15mB02ArJ/qJw/VSQK2J7RiR4+b6HYpdzQnIysCgYEAx25dTQqskQqsx/orJzuUqfNv/C0W4vqfz1eL3akFrdK+YqghXKFsDmh61JpTrTKnRLAdQeyOrhKwbNsdxSEEaeeLayKLVlimoFXGd/LZb5LQiwFcrvTzhnB+FLmFgqTnuLkpfY1woHEwSW9TpJewjbT9S6g0L2uh223nVXuLMY0CgYEA3pMOlmMGtvbEoTSuRBDNb2rmZm4zbfrcijgxRAWWZCtiFL68FU5LJLBVK2nw09sot1cabZCOuhdzxhFymRneZs73+5y8eV17DV2VnvA3HIiI5dQD/YzFDECm7ceqtiOylLUHKGZqSn0ETMaTkzxzpIKg4qxPm+RE3jMIZ+J5uJsCgYBk2iUIrtsxxgo2Xwavomu9vkPlbQ/j3QYwHn+2qqEalDZ/QbMNWvyAFMn49cpXDgSUsdM54V0OHpllkzFs3ROUUumoViHMmqw47OefBQp8Z+xaP2gVef4lAIJiDKe9t5MPUWPwADTyjgrzN/8+fw9juiFVv0wUpwOFKgEQs5diiQKBgC6RpZESc5Nl4nHrDvIl5n/zYED6BaXoLl15NhcoBudt5SIRO/RpvBW69A7aE/UK6p7WXjq4mP1ssIWz4KgATCoXUgYvn0a7Ql79r/CMce6/FvcuweED6u6bD0kdXuYhe8fR9IPmLfnnb4Cx3JOJeRZbiBSP5HOZJ7nsKibxcgPm" + ], + "keyUse": ["SIG"], + "certificate": [ + "MIIClzCCAX8CBgGIQhOHjjANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDAR0ZXN0MB4XDTIzMDUyMjA2MDczNloXDTMzMDUyMjA2MDkxNlowDzENMAsGA1UEAwwEdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMSLM0Ar/G197ClyVbbiyVenB/7oZM8XExCHA3u8tmNkhTNMdpxesES5PEJLyK0ZnpYpXoFCCbbamcS1zKccFCRos3oR0DKzgszaBgWXVWp6Cu5zWufhTmLZby+BsTX8M0t+rKdz5ykIhr+UO0Rrj7YYyxX6k5mMf+xkwyQuLyxFuUtNVcv13PRROBbE9IZ5vdZUVo6aXPfPdzOUFMv0z7vLnSLjVfOyqOcoiMUKnD4gEFyWjeAshmg3GIYc8x3cVabSJNhb7cYaZjca27ZgQsf6Pf6wQ0DU4rPBMeiLVgtj7wte1D78jnGeBb/6jtC+iWdJq6R/eQng1NZBD/iima8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAe0Bo1UpGfpOlJiVhp0XWExm8bdxFgXOU2M5XeZBsWAqBehvJkzn+tbAtlVNiIiN58XFFpH+xLZ2nJIZR5FHeCD3bYAgK72j5k45HJI95vPyslelfT/m3Np78+1iUa1U1WxN40JaowP1EeTkk5O8Pk4zTQ1Ne1usmKd+SJxI1KWN0kKuVFMmdNRb5kQKWeQvOSlWl7rd4bvHGvVnxgcPC1bshEJKRt+VpaUjpm6CKd8C3Kt7IWfIX4HTVhKZkmLn7qv6aSfwWelwZfLdaXcLXixqzqNuUk/VWbF9JT4iiag9F3mt7xryIkoRp1AEjCA82HqK72F4JCFyOhCiGrMfKJw==" + ], + "priority": ["100"] + } + }, + { + "id": "f30af2d2-d042-43b8-bc6d-22f6bab6934c", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "kid": ["6f0d9688-e974-42b4-9d84-8d098c51007c"], + "secret": [ + "8nruwD66Revr9k21e-BHtcyvNzAMFOsstxSAB0Gdy2qe2qGRm2kYOwsPzrH9ZQSdj2041SraKo6a3SHvCyTBAQ" + ], + "priority": ["100"], + "algorithm": ["HS256"] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "94c65ba1-ba50-4be2-94c4-de656145eb67", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "3b706ddf-c4b6-498a-803c-772878bc9bc3", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "basic-auth-otp", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "9ea0b8f6-882c-45ad-9110-78adf5a5d233", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "99c5ba83-b585-4601-b740-1a26670bf4e9", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "65b73dec-7dd1-4de8-b542-a023b7104afc", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "9a26b76f-da95-43f1-8da3-16c4a0654f07", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "0a77285e-d7d5-4b6c-aa9a-3eadb5e7e3d3", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "cb6c0b3b-2f5f-4493-9d14-6130f8b58dd7", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "0fd3db1b-e93d-4768-82ca-a1498ddc11d0", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "86610e70-f9f5-4c11-8a9e-9de1770565fb", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "f6aa23dd-8532-4d92-9780-3ea226481e3b", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "4d2caf65-1703-4ddb-8890-70232e91bcd8", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "eaa20c41-5334-4fb4-8c45-fb9cc71f7f74", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "b9febfb1-f0aa-4590-b782-272a4aa11575", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "03bb6ff4-eccb-4f2f-8953-3769f78c3bf3", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "38385189-246b-4ea0-ac05-d49dfe1709da", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Authentication Options", + "userSetupAllowed": false + } + ] + }, + { + "id": "1022f3c2-0469-41c9-861e-918908f103df", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "00d36c3b-e1dc-41f8-bfd0-5f8c80ea07e8", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-profile-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "4374c16e-8c65-4168-94c2-df1ab3f3e6ad", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "04d6ed6a-76c9-41fb-9074-bff8a80c2286", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "e7bad67d-1236-430a-a327-9194f9d1e2b0", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "287b5989-a927-4cf5-8067-74594ce19bc1", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DevicePollingInterval": "5", + "clientOfflineSessionMaxLifespan": "0", + "clientSessionIdleTimeout": "0", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5", + "cibaExpiresIn": "120", + "oauth2DeviceCodeLifespan": "600", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "frontendUrl": "" + }, + "keycloakVersion": "19.0.3", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} diff --git a/dev-env/nginx.conf b/dev-env/nginx.conf index e74dfe423..50af2468c 100644 --- a/dev-env/nginx.conf +++ b/dev-env/nginx.conf @@ -8,6 +8,31 @@ http { proxy_pass http://dataverse:8080; } + # https://www.keycloak.org/server/reverseproxy + location /realms { + proxy_pass http://keycloak:9080; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + } + + location /resources { + proxy_pass http://keycloak:9080; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + } + + location /admin { + proxy_pass http://keycloak:9080; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + } + location /spa { proxy_pass http://frontend:5173; proxy_http_version 1.1; diff --git a/package-lock.json b/package-lock.json index 94b01cf53..58199b7e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "react-infinite-scroll-hook": "4.1.1", "react-loader-spinner": "5.3.4", "react-markdown": "8.0.7", + "react-oauth2-code-pkce": "1.20.2", "react-router-dom": "6.23.1", "react-topbar-progress-indicator": "4.1.1", "sass": "1.58.1", @@ -35897,6 +35898,14 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, + "node_modules/react-oauth2-code-pkce": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/react-oauth2-code-pkce/-/react-oauth2-code-pkce-1.20.2.tgz", + "integrity": "sha512-ZnGX7I4opkTdXK3OXY7fIJTTSia3wy/pmMf2a/TPcQ8ZlYhNe5HiOf2DYOcpqROQttmmKaZ8OnNd28A/Umv5ew==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-property": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz", diff --git a/package.json b/package.json index 0a0b77593..1b0fcf213 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "react-markdown": "8.0.7", "react-router-dom": "6.23.1", "react-topbar-progress-indicator": "4.1.1", + "react-oauth2-code-pkce": "1.20.2", "sass": "1.58.1", "typescript": "4.9.5", "use-deep-compare": "1.2.1", diff --git a/src/SecuredApp.tsx b/src/SecuredApp.tsx new file mode 100644 index 000000000..213795545 --- /dev/null +++ b/src/SecuredApp.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import App from './App' +import { AuthProvider, TAuthConfig } from 'react-oauth2-code-pkce' + +const authConfig: TAuthConfig = { + clientId: 'test', + authorizationEndpoint: 'http://localhost:8000/realms/test/protocol/openid-connect/auth', + tokenEndpoint: 'http://localhost:8000/realms/test/protocol/openid-connect/token', + logoutEndpoint: 'http://localhost:8000/realms/test/protocol/openid-connect/logout', + redirectUri: 'http://localhost:8000/spa', + scope: 'openid' +} + +class SecuredApp extends React.Component { + public render() { + return ( + + + + ) + } +} + +export default SecuredApp diff --git a/src/index.tsx b/src/index.tsx index ba839fec8..95de8355d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,9 +1,9 @@ import React from 'react' import ReactDOM from 'react-dom/client' -import App from './App' import './i18n' import { LoadingProvider } from './sections/loading/LoadingProvider' import { ThemeProvider } from '@iqss/dataverse-design-system' +import SecuredApp from './SecuredApp' const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) root.render( @@ -11,7 +11,7 @@ root.render( - + From 5bc3cc6e9ad6178cfb286195a7a9e6294df88489 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 3 Oct 2024 17:57:34 +0100 Subject: [PATCH 02/97] Changed: simpler OIDC config init --- src/App.tsx | 18 +++++++++++++++--- src/SecuredApp.tsx | 24 ------------------------ src/index.tsx | 4 ++-- 3 files changed, 17 insertions(+), 29 deletions(-) delete mode 100644 src/SecuredApp.tsx diff --git a/src/App.tsx b/src/App.tsx index 080ff35c1..3730ba7ba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import { UserJSDataverseRepository } from './users/infrastructure/repositories/U import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' import { BASE_URL } from './config' import 'react-loading-skeleton/dist/skeleton.css' +import { AuthProvider, TAuthConfig } from 'react-oauth2-code-pkce' if (BASE_URL === '') { throw Error('VITE_DATAVERSE_BACKEND_URL environment variable should be specified.') @@ -12,12 +13,23 @@ if (BASE_URL === '') { ApiConfig.init(`${BASE_URL}/api/v1`, DataverseApiAuthMechanism.SESSION_COOKIE) } +const authConfig: TAuthConfig = { + clientId: 'test', + authorizationEndpoint: 'http://localhost:8000/realms/test/protocol/openid-connect/auth', + tokenEndpoint: 'http://localhost:8000/realms/test/protocol/openid-connect/token', + logoutEndpoint: 'http://localhost:8000/realms/test/protocol/openid-connect/logout', + redirectUri: 'http://localhost:8000/spa', + scope: 'openid' +} + const userRepository = new UserJSDataverseRepository() function App() { return ( - - - + + + + + ) } diff --git a/src/SecuredApp.tsx b/src/SecuredApp.tsx deleted file mode 100644 index 213795545..000000000 --- a/src/SecuredApp.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react' -import App from './App' -import { AuthProvider, TAuthConfig } from 'react-oauth2-code-pkce' - -const authConfig: TAuthConfig = { - clientId: 'test', - authorizationEndpoint: 'http://localhost:8000/realms/test/protocol/openid-connect/auth', - tokenEndpoint: 'http://localhost:8000/realms/test/protocol/openid-connect/token', - logoutEndpoint: 'http://localhost:8000/realms/test/protocol/openid-connect/logout', - redirectUri: 'http://localhost:8000/spa', - scope: 'openid' -} - -class SecuredApp extends React.Component { - public render() { - return ( - - - - ) - } -} - -export default SecuredApp diff --git a/src/index.tsx b/src/index.tsx index 95de8355d..4d8317f1e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,7 +3,7 @@ import ReactDOM from 'react-dom/client' import './i18n' import { LoadingProvider } from './sections/loading/LoadingProvider' import { ThemeProvider } from '@iqss/dataverse-design-system' -import SecuredApp from './SecuredApp' +import App from './App' const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) root.render( @@ -11,7 +11,7 @@ root.render( - + From d5b41b2e13a2605260f98307d472de0a67f3ced0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 4 Oct 2024 12:39:41 -0300 Subject: [PATCH 03/97] chore: use alpha version of js-dataverse --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 58199b7e3..d69d34284 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-pr192.6406015", + "@iqss/dataverse-client-javascript": "2.0.0-alpha.1", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -3675,9 +3675,9 @@ }, "node_modules/@iqss/dataverse-client-javascript": { "name": "@IQSS/dataverse-client-javascript", - "version": "2.0.0-pr192.6406015", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr192.6406015/4d6562b1e287c872e92ef3a551ee8aa63c5262b5", - "integrity": "sha512-1DmypaaV1cXS4y+kbx33GLmRLwI/8Cwj82MLhpq9gdy7+racUOyzVs3MFKJmdLLWTy7pAWKtj6a+c8Pt6DmtzQ==", + "version": "2.0.0-alpha.1", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-alpha.1/5c1b7dc4ae62ec38d5a93c79ec2452b5de0bb729", + "integrity": "sha512-ItrxNcTlcxBy/4baX05dujMdBrMYAhePOAbNRJv+aspPmv5KSua3FkiM1vm+7mAZbR7x+6nRjmejZxcGwhN0aA==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", diff --git a/package.json b/package.json index 1b0fcf213..1e56e15ee 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-pr192.6406015", + "@iqss/dataverse-client-javascript": "2.0.0-alpha.1", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", From 8e80e3d46d842f477ac0a72e0124b324a04d2c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 4 Oct 2024 16:35:32 -0300 Subject: [PATCH 04/97] feat: avoid react strict mode for now to avoid double renders on dev mode --- src/index.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 4d8317f1e..f220c2fec 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,13 +7,13 @@ import App from './App' const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) root.render( - - - - - - - - - + // + + + + + + + + // ) From ee4d48fbf8579ac64ff3b7fa6de8cbe641a0097b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 4 Oct 2024 16:37:11 -0300 Subject: [PATCH 05/97] feat: autoLogin config to false to avoid inmediate login from AuthContext --- src/App.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 3730ba7ba..201a62429 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,7 +19,8 @@ const authConfig: TAuthConfig = { tokenEndpoint: 'http://localhost:8000/realms/test/protocol/openid-connect/token', logoutEndpoint: 'http://localhost:8000/realms/test/protocol/openid-connect/logout', redirectUri: 'http://localhost:8000/spa', - scope: 'openid' + scope: 'openid', + autoLogin: false } const userRepository = new UserJSDataverseRepository() From 1edfce948bdb8b08aecffbc53c5bd83ecf82286b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 4 Oct 2024 16:47:35 -0300 Subject: [PATCH 06/97] feat: modify protected route base on AuthContext properties --- src/router/ProtectedRoute.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/router/ProtectedRoute.tsx b/src/router/ProtectedRoute.tsx index ca7ae4886..2e5eb58aa 100644 --- a/src/router/ProtectedRoute.tsx +++ b/src/router/ProtectedRoute.tsx @@ -1,23 +1,18 @@ +import { useContext } from 'react' import { Outlet } from 'react-router-dom' -import { Route } from '../sections/Route.enum' -import { useSession } from '../sections/session/SessionContext' +import { AuthContext } from 'react-oauth2-code-pkce' import { AppLoader } from '../sections/shared/layout/app-loader/AppLoader' -import { BASE_URL } from '../config' export const ProtectedRoute = () => { - const { user, isLoadingUser } = useSession() + const { token, loginInProgress, logIn } = useContext(AuthContext) - if (isLoadingUser) { + if (loginInProgress) { return } - if (!user) { - window.location.href = `${BASE_URL}${Route.LOG_IN}` - return null + if (!token) { + logIn() } - // When we have the login page inside the SPA, we can use the following code: - // return !user ? : - return } From d11f55e64fe0ac6b2d10125058fdaf0c339d13b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 4 Oct 2024 17:14:09 -0300 Subject: [PATCH 07/97] feat: login, logout and user name in header with OIDC AuthContext functions and data --- src/sections/layout/header/Header.module.scss | 7 +++++ src/sections/layout/header/Header.tsx | 29 ++++++++++++++----- .../layout/header/LoggedInHeaderActions.tsx | 25 +++++++++++----- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/sections/layout/header/Header.module.scss b/src/sections/layout/header/Header.module.scss index 3a24ccd5b..72c4a39eb 100644 --- a/src/sections/layout/header/Header.module.scss +++ b/src/sections/layout/header/Header.module.scss @@ -1,3 +1,10 @@ +@import 'node_modules/@iqss/dataverse-design-system/src/lib/assets/styles/design-tokens/colors.module'; + .navbar { box-shadow: 0 1px 5px rgba(0 0 0 / 10%); + + .login-btn { + color: $dv-subtext-color; + text-decoration: none; + } } diff --git a/src/sections/layout/header/Header.tsx b/src/sections/layout/header/Header.tsx index 092b7b8ef..4a047327f 100644 --- a/src/sections/layout/header/Header.tsx +++ b/src/sections/layout/header/Header.tsx @@ -1,9 +1,10 @@ +import { useContext } from 'react' +import { AuthContext } from 'react-oauth2-code-pkce' import dataverse_logo from '../../../assets/dataverse_brand_icon.svg' import { useTranslation } from 'react-i18next' -import { Navbar } from '@iqss/dataverse-design-system' +import { Button, Navbar } from '@iqss/dataverse-design-system' import { Route } from '../../Route.enum' -import { useSession } from '../../session/SessionContext' -import { BASE_URL } from '../../../config' +// import { BASE_URL } from '../../../config' import { LoggedInHeaderActions } from './LoggedInHeaderActions' import { CollectionJSDataverseRepository } from '../../../collection/infrastructure/repositories/CollectionJSDataverseRepository' import styles from './Header.module.scss' @@ -12,7 +13,13 @@ const collectionRepository = new CollectionJSDataverseRepository() export function Header() { const { t } = useTranslation('header') - const { user } = useSession() + + // tokenData is originally typed as Record but we know it has a name property (this will need a double check in future iterations) + const { token, tokenData, logIn: oidcLogin } = useContext(AuthContext) + + const handleOidcLogIn = () => { + oidcLogin() + } return ( - {user ? ( - + {token && tokenData ? ( + ) : ( <> - {t('logIn')} - {t('signUp')} + + {/* {t('logIn')} */} + {/* {t('signUp')} */} )} diff --git a/src/sections/layout/header/LoggedInHeaderActions.tsx b/src/sections/layout/header/LoggedInHeaderActions.tsx index 1d44f715e..b7b8a762f 100644 --- a/src/sections/layout/header/LoggedInHeaderActions.tsx +++ b/src/sections/layout/header/LoggedInHeaderActions.tsx @@ -1,10 +1,11 @@ +import { useContext } from 'react' +import { AuthContext } from 'react-oauth2-code-pkce' import { useTranslation } from 'react-i18next' import { Link, useNavigate } from 'react-router-dom' import { Navbar } from '@iqss/dataverse-design-system' import { useGetCollectionUserPermissions } from '../../../shared/hooks/useGetCollectionUserPermissions' import { useSession } from '../../session/SessionContext' import { RouteWithParams, Route } from '../../Route.enum' -import { User } from '../../../users/domain/models/User' import { CollectionRepository } from '../../../collection/domain/repositories/CollectionRepository' import { ROOT_COLLECTION_ALIAS } from '../../../collection/domain/models/Collection' import { AccountHelper } from '../../account/AccountHelper' @@ -12,24 +13,31 @@ import { AccountHelper } from '../../account/AccountHelper' const currentPage = 0 interface LoggedInHeaderActionsProps { - user: User + userName: string collectionRepository: CollectionRepository } export const LoggedInHeaderActions = ({ - user, + userName, collectionRepository }: LoggedInHeaderActionsProps) => { const { t } = useTranslation('header') const { logout } = useSession() const navigate = useNavigate() + const { logOut: oidcLogout } = useContext(AuthContext) + const { collectionUserPermissions } = useGetCollectionUserPermissions({ collectionIdOrAlias: ROOT_COLLECTION_ALIAS, collectionRepository: collectionRepository }) - const onLogoutClick = () => { + // Just keeping logOut in a handler function because we might want to add more logic here (e.g: logOut can receive up to 3 parameters) + const handleOidcLogout = () => { + oidcLogout() + } + + const _handleSessionLogout = () => { void logout().then(() => { navigate(currentPage) }) @@ -54,15 +62,18 @@ export const LoggedInHeaderActions = ({ {t('navigation.newDataset')} - + {t('navigation.apiToken')} - - {t('logOut')} + + OIDC {t('logOut')} + {/* + {t('logOut')} + */} ) From 454ddb423341480e2accdf1327211eaba0a31336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 4 Oct 2024 17:14:32 -0300 Subject: [PATCH 08/97] feat: some experimental things and logs in the SessionProvider --- src/sections/session/SessionProvider.tsx | 58 +++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/src/sections/session/SessionProvider.tsx b/src/sections/session/SessionProvider.tsx index 6a2152c38..17e124920 100644 --- a/src/sections/session/SessionProvider.tsx +++ b/src/sections/session/SessionProvider.tsx @@ -1,17 +1,73 @@ -import { PropsWithChildren, useEffect, useState } from 'react' +import { PropsWithChildren, useContext, useEffect, useState } from 'react' +import { AuthContext } from 'react-oauth2-code-pkce' import { User } from '../../users/domain/models/User' import { SessionContext } from './SessionContext' import { getUser } from '../../users/domain/useCases/getUser' import { UserRepository } from '../../users/domain/repositories/UserRepository' import { logOut } from '../../users/domain/useCases/logOut' +const BACKEND_URL = import.meta.env.VITE_DATAVERSE_BACKEND_URL as string + interface SessionProviderProps { repository: UserRepository } export function SessionProvider({ repository, children }: PropsWithChildren) { + const { token, tokenData } = useContext(AuthContext) const [user, setUser] = useState(null) const [isLoadingUser, setIsLoadingUser] = useState(true) + useEffect(() => { + // Just to log some data from the AuthContext + console.log( + '%cToken from AuthContext: ', + 'background: green; color: white; padding: 2px; border-radius: 2px;', + { + token + } + ) + + console.log( + '%cTokenData from AuthContext: ', + 'background: green; color: white; padding: 2px; border-radius: 2px;', + tokenData + ) + }, [token, tokenData]) + + useEffect(() => { + if (token) { + fetch(`${BACKEND_URL}/api/v1/users/:me`, { + method: 'GET', + credentials: 'omit', // to avoid sending the cookie + headers: { + // 👇 This throws Error 400: User with token null not found. + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + + // 👇 And this throws BAD API key because its a token not an api key of course + // 'X-Dataverse-key': token + } + }) + .then((response) => { + if (!response.ok) { + return response.json().then((errData: Error) => { + throw new Error(`Error ${response.status}: ${errData.message || response.statusText}`) + }) + } + return response.json() + }) + .then((data) => { + console.log('User data:', data) + }) + .catch((error) => { + console.error( + '%cError getting user data with users/:me endpoint', + 'background: #eb5656; color: white; padding: 2px', + error + ) + }) + } + }, [token]) + useEffect(() => { const handleGetUser = async () => { setIsLoadingUser(true) From da681ad4de0a7a7b1b58fcae4896aefe59376bb8 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 7 Oct 2024 10:57:41 +0100 Subject: [PATCH 09/97] Fixed: reverse proxy rules --- dev-env/nginx.conf | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/dev-env/nginx.conf b/dev-env/nginx.conf index 50af2468c..9c02e0a82 100644 --- a/dev-env/nginx.conf +++ b/dev-env/nginx.conf @@ -2,13 +2,14 @@ events {} http { server { listen 80; - server_name localhost; + server_name localhost; + # Default route for other URLs location / { proxy_pass http://dataverse:8080; } - # https://www.keycloak.org/server/reverseproxy + # Keycloak reverse proxy for /realms location /realms { proxy_pass http://keycloak:9080; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -17,6 +18,37 @@ http { proxy_set_header X-Forwarded-Port $server_port; } + # Specific route for /resources/images + location /resources/images { + proxy_pass http://dataverse:8080; + } + + # Specific route for /resources/css + location /resources/css { + proxy_pass http://dataverse:8080; + } + + # Specific route for /resources/js + location /resources/js { + proxy_pass http://dataverse:8080; + } + + # Specific route for /resources/dev + location /resources/dev { + proxy_pass http://dataverse:8080; + } + + # Specific route for /resources/fontcustom + location /resources/fontcustom { + proxy_pass http://dataverse:8080; + } + + # Specific route for /resources/iqbs + location /resources/iqbs { + proxy_pass http://dataverse:8080; + } + + # General route for other /resources routes, handled by Keycloak location /resources { proxy_pass http://keycloak:9080; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -25,6 +57,7 @@ http { proxy_set_header X-Forwarded-Port $server_port; } + # Keycloak reverse proxy for /admin location /admin { proxy_pass http://keycloak:9080; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -33,6 +66,7 @@ http { proxy_set_header X-Forwarded-Port $server_port; } + # Route for SPA frontend location /spa { proxy_pass http://frontend:5173; proxy_http_version 1.1; From 78ef74ac58b44c0af2de2c18b0d9c6d473c7d128 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 7 Oct 2024 11:18:56 +0100 Subject: [PATCH 10/97] Added: OIDC support for the containerized Dataverse instance --- dev-env/docker-compose-dev.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dev-env/docker-compose-dev.yml b/dev-env/docker-compose-dev.yml index db3b1888e..9370e6bcf 100644 --- a/dev-env/docker-compose-dev.yml +++ b/dev-env/docker-compose-dev.yml @@ -48,6 +48,10 @@ services: DATAVERSE_DB_PASSWORD: secret DATAVERSE_DB_USER: ${DATAVERSE_DB_USER} DATAVERSE_FEATURE_API_SESSION_AUTH: 1 + DATAVERSE_AUTH_OIDC_ENABLED: "1" + DATAVERSE_AUTH_OIDC_CLIENT_ID: test + DATAVERSE_AUTH_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 + DATAVERSE_AUTH_OIDC_AUTH_SERVER_URL: http://localhost:8000/realms/test JVM_ARGS: -Ddataverse.pid.providers=fake -Ddataverse.pid.default-provider=fake -Ddataverse.pid.fake.type=FAKE From cb435092ff3c820e250003d8526136c0ea845183 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 7 Oct 2024 11:36:57 +0100 Subject: [PATCH 11/97] Added: missing Bearer token feature flag turned on --- dev-env/docker-compose-dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-env/docker-compose-dev.yml b/dev-env/docker-compose-dev.yml index 9370e6bcf..1a2a9193e 100644 --- a/dev-env/docker-compose-dev.yml +++ b/dev-env/docker-compose-dev.yml @@ -48,6 +48,7 @@ services: DATAVERSE_DB_PASSWORD: secret DATAVERSE_DB_USER: ${DATAVERSE_DB_USER} DATAVERSE_FEATURE_API_SESSION_AUTH: 1 + DATAVERSE_FEATURE_API_BEARER_AUTH: "1" DATAVERSE_AUTH_OIDC_ENABLED: "1" DATAVERSE_AUTH_OIDC_CLIENT_ID: test DATAVERSE_AUTH_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 From 5b07eb4073c26c82ed53e658928c3d952479ed8c Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 7 Oct 2024 14:01:04 +0100 Subject: [PATCH 12/97] Added: format tweak in docker-compose --- dev-env/docker-compose-dev.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-env/docker-compose-dev.yml b/dev-env/docker-compose-dev.yml index 1a2a9193e..e14f5c4c5 100644 --- a/dev-env/docker-compose-dev.yml +++ b/dev-env/docker-compose-dev.yml @@ -48,8 +48,8 @@ services: DATAVERSE_DB_PASSWORD: secret DATAVERSE_DB_USER: ${DATAVERSE_DB_USER} DATAVERSE_FEATURE_API_SESSION_AUTH: 1 - DATAVERSE_FEATURE_API_BEARER_AUTH: "1" - DATAVERSE_AUTH_OIDC_ENABLED: "1" + DATAVERSE_FEATURE_API_BEARER_AUTH: 1 + DATAVERSE_AUTH_OIDC_ENABLED: 1 DATAVERSE_AUTH_OIDC_CLIENT_ID: test DATAVERSE_AUTH_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 DATAVERSE_AUTH_OIDC_AUTH_SERVER_URL: http://localhost:8000/realms/test From ba147051a7ec8eafe7d642760f407a38a4e4d7a5 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 7 Oct 2024 17:47:05 +0100 Subject: [PATCH 13/97] Removed: session auth feature flag from docker-compose due to incompatibility with bearer auth feature flag --- dev-env/docker-compose-dev.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/dev-env/docker-compose-dev.yml b/dev-env/docker-compose-dev.yml index e14f5c4c5..3e1a5dc04 100644 --- a/dev-env/docker-compose-dev.yml +++ b/dev-env/docker-compose-dev.yml @@ -47,7 +47,6 @@ services: DATAVERSE_DB_HOST: postgres DATAVERSE_DB_PASSWORD: secret DATAVERSE_DB_USER: ${DATAVERSE_DB_USER} - DATAVERSE_FEATURE_API_SESSION_AUTH: 1 DATAVERSE_FEATURE_API_BEARER_AUTH: 1 DATAVERSE_AUTH_OIDC_ENABLED: 1 DATAVERSE_AUTH_OIDC_CLIENT_ID: test From 4cbaa12d91e782f6f06dfbb0cfe2d471da317e34 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 8 Oct 2024 12:45:37 +0100 Subject: [PATCH 14/97] Fixed: OIDC integration issues --- dev-env/docker-compose-dev.yml | 6 ++++-- src/sections/session/SessionProvider.tsx | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dev-env/docker-compose-dev.yml b/dev-env/docker-compose-dev.yml index 3e1a5dc04..c782fd0c6 100644 --- a/dev-env/docker-compose-dev.yml +++ b/dev-env/docker-compose-dev.yml @@ -51,7 +51,7 @@ services: DATAVERSE_AUTH_OIDC_ENABLED: 1 DATAVERSE_AUTH_OIDC_CLIENT_ID: test DATAVERSE_AUTH_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 - DATAVERSE_AUTH_OIDC_AUTH_SERVER_URL: http://localhost:8000/realms/test + DATAVERSE_AUTH_OIDC_AUTH_SERVER_URL: http://keycloak.mydomain.com:9080/realms/test JVM_ARGS: -Ddataverse.pid.providers=fake -Ddataverse.pid.default-provider=fake -Ddataverse.pid.fake.type=FAKE @@ -193,7 +193,9 @@ services: - KEYCLOAK_ADMIN_PASSWORD=kcpassword - KEYCLOAK_LOGLEVEL=DEBUG networks: - - dataverse + dataverse: + aliases: + - keycloak.mydomain.com expose: - 9080 volumes: diff --git a/src/sections/session/SessionProvider.tsx b/src/sections/session/SessionProvider.tsx index 17e124920..ea1f362ae 100644 --- a/src/sections/session/SessionProvider.tsx +++ b/src/sections/session/SessionProvider.tsx @@ -42,7 +42,6 @@ export function SessionProvider({ repository, children }: PropsWithChildren Date: Tue, 8 Oct 2024 10:37:41 -0300 Subject: [PATCH 15/97] feat: change base url naming of backend url due to conflict with basename path --- src/App.tsx | 29 ++++++++++++------- src/config.ts | 2 +- .../FileJSDataverseRepository.ts | 4 +-- src/sections/Route.enum.ts | 6 ++-- .../file/file-metadata/FileMetadata.tsx | 4 +-- .../file/file-metadata/FileMetadata.spec.tsx | 4 +-- tests/e2e-integration/shared/TestsUtils.ts | 4 +-- tests/support/e2e.ts | 4 +-- 8 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 201a62429..867a139d8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,30 +1,39 @@ +import { AuthProvider, TAuthConfig, TRefreshTokenExpiredEvent } from 'react-oauth2-code-pkce' import { ApiConfig } from '@iqss/dataverse-client-javascript/dist/core' +import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' import { Router } from './router' import { SessionProvider } from './sections/session/SessionProvider' import { UserJSDataverseRepository } from './users/infrastructure/repositories/UserJSDataverseRepository' -import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' -import { BASE_URL } from './config' +import { Route } from './sections/Route.enum' +import { DATAVERSE_BACKEND_URL } from './config' import 'react-loading-skeleton/dist/skeleton.css' -import { AuthProvider, TAuthConfig } from 'react-oauth2-code-pkce' -if (BASE_URL === '') { +if (DATAVERSE_BACKEND_URL === '') { throw Error('VITE_DATAVERSE_BACKEND_URL environment variable should be specified.') } else { - ApiConfig.init(`${BASE_URL}/api/v1`, DataverseApiAuthMechanism.SESSION_COOKIE) + ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.SESSION_COOKIE) } +const origin = window.location.origin +const BASENAME_URL = import.meta.env.BASE_URL ?? '' + const authConfig: TAuthConfig = { clientId: 'test', - authorizationEndpoint: 'http://localhost:8000/realms/test/protocol/openid-connect/auth', - tokenEndpoint: 'http://localhost:8000/realms/test/protocol/openid-connect/token', - logoutEndpoint: 'http://localhost:8000/realms/test/protocol/openid-connect/logout', - redirectUri: 'http://localhost:8000/spa', + authorizationEndpoint: `${origin}/realms/test/protocol/openid-connect/auth`, + tokenEndpoint: `${origin}/realms/test/protocol/openid-connect/token`, + logoutEndpoint: `${origin}/realms/test/protocol/openid-connect/logout`, + logoutRedirect: `${origin}${BASENAME_URL}`, + redirectUri: `${origin}${BASENAME_URL}${Route.AUTH_CALLBACK}`, scope: 'openid', - autoLogin: false + onRefreshTokenExpire: (event: TRefreshTokenExpiredEvent) => + event.logIn(undefined, undefined, 'popup'), + autoLogin: false, + clearURL: false } const userRepository = new UserJSDataverseRepository() function App() { + console.log({ authConfig }) return ( diff --git a/src/config.ts b/src/config.ts index 9ea389068..4d4037d3e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1 +1 @@ -export const BASE_URL = (import.meta.env.VITE_DATAVERSE_BACKEND_URL as string) ?? '' +export const DATAVERSE_BACKEND_URL = (import.meta.env.VITE_DATAVERSE_BACKEND_URL as string) ?? '' diff --git a/src/files/infrastructure/FileJSDataverseRepository.ts b/src/files/infrastructure/FileJSDataverseRepository.ts index 2b204a17f..27552ddb9 100644 --- a/src/files/infrastructure/FileJSDataverseRepository.ts +++ b/src/files/infrastructure/FileJSDataverseRepository.ts @@ -25,7 +25,7 @@ import { JSFileMapper } from './mappers/JSFileMapper' import { DatasetVersion, DatasetVersionNumber } from '../../dataset/domain/models/Dataset' import { File } from '../domain/models/File' import { FilePaginationInfo } from '../domain/models/FilePaginationInfo' -import { BASE_URL } from '../../config' +import { DATAVERSE_BACKEND_URL } from '../../config' import { FilePreview } from '../domain/models/FilePreview' import { JSFilesCountInfoMapper } from './mappers/JSFilesCountInfoMapper' import { JSFileMetadataMapper } from './mappers/JSFileMetadataMapper' @@ -37,7 +37,7 @@ import { FileHolder } from '../domain/models/FileHolder' const includeDeaccessioned = true export class FileJSDataverseRepository implements FileRepository { - static readonly DATAVERSE_BACKEND_URL = BASE_URL + static readonly DATAVERSE_BACKEND_URL = DATAVERSE_BACKEND_URL getAllByDatasetPersistentId( datasetPersistentId: string, diff --git a/src/sections/Route.enum.ts b/src/sections/Route.enum.ts index cca272366..70905d5e6 100644 --- a/src/sections/Route.enum.ts +++ b/src/sections/Route.enum.ts @@ -13,7 +13,8 @@ export enum Route { COLLECTIONS_BASE = '/collections', COLLECTIONS = '/collections/:collectionId', CREATE_COLLECTION = '/collections/:ownerCollectionId/create', - ACCOUNT = '/account' + ACCOUNT = '/account', + AUTH_CALLBACK = '/auth-callback' } export const RouteWithParams = { @@ -27,5 +28,6 @@ export const RouteWithParams = { export enum QueryParamKey { VERSION = 'version', PERSISTENT_ID = 'persistentId', - QUERY = 'q' + QUERY = 'q', + AUTH_STATE = 'state' } diff --git a/src/sections/file/file-metadata/FileMetadata.tsx b/src/sections/file/file-metadata/FileMetadata.tsx index 2655d2d12..62af5b68a 100644 --- a/src/sections/file/file-metadata/FileMetadata.tsx +++ b/src/sections/file/file-metadata/FileMetadata.tsx @@ -4,7 +4,7 @@ import { FileLabels } from '../file-labels/FileLabels' import styles from './FileMetadata.module.scss' import { DateHelper } from '../../../shared/helpers/DateHelper' import { FileEmbargoDate } from '../file-embargo/FileEmbargoDate' -import { BASE_URL } from '../../../config' +import { DATAVERSE_BACKEND_URL } from '../../../config' import { Trans, useTranslation } from 'react-i18next' import { FileMetadata as FileMetadataModel } from '../../../files/domain/models/FileMetadata' import { FilePermissions } from '../../../files/domain/models/FilePermissions' @@ -71,7 +71,7 @@ export function FileMetadata({

- {BASE_URL} + {DATAVERSE_BACKEND_URL} {removeQueryParams(metadata.downloadUrls.original)} diff --git a/tests/component/sections/file/file-metadata/FileMetadata.spec.tsx b/tests/component/sections/file/file-metadata/FileMetadata.spec.tsx index 7e2e09a1e..ad8a5bb79 100644 --- a/tests/component/sections/file/file-metadata/FileMetadata.spec.tsx +++ b/tests/component/sections/file/file-metadata/FileMetadata.spec.tsx @@ -1,6 +1,6 @@ import { FileMetadata } from '../../../../../src/sections/file/file-metadata/FileMetadata' import { FileMother } from '../../../files/domain/models/FileMother' -import { BASE_URL } from '../../../../../src/config' +import { DATAVERSE_BACKEND_URL } from '../../../../../src/config' import { FileSizeUnit } from '../../../../../src/files/domain/models/FileMetadata' import { FileEmbargoMother, @@ -118,7 +118,7 @@ describe('FileMetadata', () => { ) cy.findByText('Download URL').should('exist') - cy.findByText(`${BASE_URL}/api/datafile/3`).should('exist') + cy.findByText(`${DATAVERSE_BACKEND_URL}/api/datafile/3`).should('exist') cy.findByText( 'Use the Download URL in a Wget command or a download manager to avoid interrupted downloads, time outs or other failures.' ).should('exist') diff --git a/tests/e2e-integration/shared/TestsUtils.ts b/tests/e2e-integration/shared/TestsUtils.ts index 5df3e1d75..78ee00b96 100644 --- a/tests/e2e-integration/shared/TestsUtils.ts +++ b/tests/e2e-integration/shared/TestsUtils.ts @@ -3,10 +3,10 @@ import { DataverseApiHelper } from './DataverseApiHelper' import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' import { UserJSDataverseRepository } from '../../../src/users/infrastructure/repositories/UserJSDataverseRepository' import { DatasetHelper } from './datasets/DatasetHelper' -import { BASE_URL } from '../../../src/config' +import { DATAVERSE_BACKEND_URL } from '../../../src/config' export class TestsUtils { - static readonly DATAVERSE_BACKEND_URL = BASE_URL + static readonly DATAVERSE_BACKEND_URL = DATAVERSE_BACKEND_URL static setup() { ApiConfig.init(`${this.DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.SESSION_COOKIE) diff --git a/tests/support/e2e.ts b/tests/support/e2e.ts index ca4fd04fb..cda0896ec 100644 --- a/tests/support/e2e.ts +++ b/tests/support/e2e.ts @@ -17,9 +17,9 @@ import '../../tests/support/commands' import { ApiConfig } from '@iqss/dataverse-client-javascript/dist/core' import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' -import { BASE_URL } from '../../src/config' +import { DATAVERSE_BACKEND_URL } from '../../src/config' -ApiConfig.init(`${BASE_URL}/api/v1`, DataverseApiAuthMechanism.SESSION_COOKIE) +ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.SESSION_COOKIE) //https://github.com/cypress-io/cypress/issues/18182 declare global { From 78b37edc7248568ecd5b5f8f3c8c529f79bf61d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 8 Oct 2024 10:45:37 -0300 Subject: [PATCH 16/97] feat: callback page, redirect user to intended page --- src/router/ProtectedRoute.tsx | 25 +++++++++++----- src/router/routes.tsx | 5 ++++ src/sections/auth-callback/AuthCallback.tsx | 32 +++++++++++++++++++++ src/sections/layout/header/Header.tsx | 12 ++++---- 4 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 src/sections/auth-callback/AuthCallback.tsx diff --git a/src/router/ProtectedRoute.tsx b/src/router/ProtectedRoute.tsx index 2e5eb58aa..799184a43 100644 --- a/src/router/ProtectedRoute.tsx +++ b/src/router/ProtectedRoute.tsx @@ -1,18 +1,29 @@ -import { useContext } from 'react' -import { Outlet } from 'react-router-dom' +import { useContext, useEffect } from 'react' +import { Outlet, useLocation } from 'react-router-dom' import { AuthContext } from 'react-oauth2-code-pkce' import { AppLoader } from '../sections/shared/layout/app-loader/AppLoader' +/** + * This component is responsible for protecting routes that require authentication. + * If we dont have a token, we redirect the user to the OIDC login page with the current pathname as a state parameter. + * This state parameter is used to redirect the user back to their former intended pathname after the OIDC login is complete. + */ + export const ProtectedRoute = () => { - const { token, loginInProgress, logIn } = useContext(AuthContext) + const { pathname } = useLocation() + const { token, loginInProgress, logIn: oidcLogin } = useContext(AuthContext) + + useEffect(() => { + if (loginInProgress) return + + if (!token) { + oidcLogin(encodeURIComponent(pathname)) + } + }, [token, oidcLogin, pathname, loginInProgress]) if (loginInProgress) { return } - if (!token) { - logIn() - } - return } diff --git a/src/router/routes.tsx b/src/router/routes.tsx index b71090f75..0f50a3f6b 100644 --- a/src/router/routes.tsx +++ b/src/router/routes.tsx @@ -12,6 +12,7 @@ import { CreateCollectionFactory } from '../sections/create-collection/CreateCol import { AccountFactory } from '../sections/account/AccountFactory' import { ProtectedRoute } from './ProtectedRoute' import { HomepageFactory } from '../sections/homepage/HomepageFactory' +import { AuthCallback } from '../sections/auth-callback/AuthCallback' export const routes: RouteObject[] = [ { @@ -39,6 +40,10 @@ export const routes: RouteObject[] = [ path: Route.FILES, element: FileFactory.create() }, + { + path: Route.AUTH_CALLBACK, + element: + }, // 🔐 Protected routes are only accessible to authenticated users { element: , diff --git a/src/sections/auth-callback/AuthCallback.tsx b/src/sections/auth-callback/AuthCallback.tsx new file mode 100644 index 000000000..66f03cb7e --- /dev/null +++ b/src/sections/auth-callback/AuthCallback.tsx @@ -0,0 +1,32 @@ +import { useContext, useEffect } from 'react' +import { useNavigate, useSearchParams } from 'react-router-dom' +import { AuthContext } from 'react-oauth2-code-pkce' +import { QueryParamKey } from '../Route.enum' +import { AppLoader } from '../shared/layout/app-loader/AppLoader' + +/** + * This component will we rendered as redirectUri page after the OIDC login is complete. + * It will redirect the user to the intended page before the OIDC login was initiated. + * If the state parameter is not present, the user will be redirected to the homepage. + */ + +export const AuthCallback = () => { + const navigate = useNavigate() + const { loginInProgress } = useContext(AuthContext) + const [searchParams] = useSearchParams() + + const stateQueryParam = searchParams.get(QueryParamKey.AUTH_STATE) + + useEffect(() => { + if (loginInProgress) return + + if (!stateQueryParam) { + navigate('/', { replace: true }) + return + } + + navigate(decodeURIComponent(stateQueryParam), { replace: true }) + }, [stateQueryParam, navigate, loginInProgress]) + + return +} diff --git a/src/sections/layout/header/Header.tsx b/src/sections/layout/header/Header.tsx index 4a047327f..b17ab166e 100644 --- a/src/sections/layout/header/Header.tsx +++ b/src/sections/layout/header/Header.tsx @@ -4,21 +4,23 @@ import dataverse_logo from '../../../assets/dataverse_brand_icon.svg' import { useTranslation } from 'react-i18next' import { Button, Navbar } from '@iqss/dataverse-design-system' import { Route } from '../../Route.enum' -// import { BASE_URL } from '../../../config' +// import { DATAVERSE_BACKEND_URL } from '../../../config' import { LoggedInHeaderActions } from './LoggedInHeaderActions' import { CollectionJSDataverseRepository } from '../../../collection/infrastructure/repositories/CollectionJSDataverseRepository' import styles from './Header.module.scss' +import { useLocation } from 'react-router-dom' const collectionRepository = new CollectionJSDataverseRepository() export function Header() { const { t } = useTranslation('header') + const { pathname } = useLocation() - // tokenData is originally typed as Record but we know it has a name property (this will need a double check in future iterations) + // TODO:ME tokenData is originally typed as Record but we know it has a name property (this will need a double check in future iterations) const { token, tokenData, logIn: oidcLogin } = useContext(AuthContext) const handleOidcLogIn = () => { - oidcLogin() + oidcLogin(encodeURIComponent(pathname)) } return ( @@ -39,8 +41,8 @@ export function Header() { - {/* {t('logIn')} */} - {/* {t('signUp')} */} + {/* {t('logIn')} */} + {/* {t('signUp')} */} )} From 00d3a0edcc3221ad76f79ed8f7964316e45ef5de Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 11 Oct 2024 18:54:55 +0100 Subject: [PATCH 17/97] Changed: using js-dataverse version with OIDC token bearer auth --- dev-env/docker-compose-dev.yml | 3 +++ package-lock.json | 8 ++++---- package.json | 2 +- src/App.tsx | 2 +- src/sections/session/SessionProvider.tsx | 1 - 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/dev-env/docker-compose-dev.yml b/dev-env/docker-compose-dev.yml index c782fd0c6..0c11807b5 100644 --- a/dev-env/docker-compose-dev.yml +++ b/dev-env/docker-compose-dev.yml @@ -73,6 +73,9 @@ services: -Ddataverse.files.s3.custom-endpoint-url=https://s3.us-east-1.amazonaws.com expose: - '8080' + # TODO: The port has been opened for the redirection to /oauth2/callback.xhtml after a JSF OIDC login. We may prefer to change this to use the proxy. + ports: + - '8080:8080' networks: - dataverse depends_on: diff --git a/package-lock.json b/package-lock.json index d69d34284..49c93c2c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-alpha.1", + "@iqss/dataverse-client-javascript": "2.0.0-pr201.68eefd3", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -3675,9 +3675,9 @@ }, "node_modules/@iqss/dataverse-client-javascript": { "name": "@IQSS/dataverse-client-javascript", - "version": "2.0.0-alpha.1", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-alpha.1/5c1b7dc4ae62ec38d5a93c79ec2452b5de0bb729", - "integrity": "sha512-ItrxNcTlcxBy/4baX05dujMdBrMYAhePOAbNRJv+aspPmv5KSua3FkiM1vm+7mAZbR7x+6nRjmejZxcGwhN0aA==", + "version": "2.0.0-pr201.68eefd3", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr201.68eefd3/7a5ae54766cb8aef9e1148eae677963f0cb7cc62", + "integrity": "sha512-OtyZu97M9VfHJbP7FacPNm6YYNGrtUq520KmXU/qRBpIT25ocAnkdIlA3InlUXixf+hROWYM8aa/4vi24TC4BA==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", diff --git a/package.json b/package.json index 1e56e15ee..d8b03c24a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-alpha.1", + "@iqss/dataverse-client-javascript": "2.0.0-pr201.68eefd3", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", diff --git a/src/App.tsx b/src/App.tsx index 201a62429..b1d2018bb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,7 +10,7 @@ import { AuthProvider, TAuthConfig } from 'react-oauth2-code-pkce' if (BASE_URL === '') { throw Error('VITE_DATAVERSE_BACKEND_URL environment variable should be specified.') } else { - ApiConfig.init(`${BASE_URL}/api/v1`, DataverseApiAuthMechanism.SESSION_COOKIE) + ApiConfig.init(`${BASE_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) } const authConfig: TAuthConfig = { diff --git a/src/sections/session/SessionProvider.tsx b/src/sections/session/SessionProvider.tsx index ea1f362ae..008d1ae90 100644 --- a/src/sections/session/SessionProvider.tsx +++ b/src/sections/session/SessionProvider.tsx @@ -39,7 +39,6 @@ export function SessionProvider({ repository, children }: PropsWithChildren Date: Mon, 14 Oct 2024 14:07:12 +0100 Subject: [PATCH 18/97] Changed: upgraded js-dataverse package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d8b03c24a..aa1ed767a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-pr201.68eefd3", + "@iqss/dataverse-client-javascript": "2.0.0-pr201.c64af18", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", From 78828e8fbb835818e4203dca40c70644526f9a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 14 Oct 2024 15:27:14 -0300 Subject: [PATCH 19/97] feat: back to using authenticated user data from dataverse --- package-lock.json | 8 +-- src/App.tsx | 2 +- src/sections/layout/header/Header.tsx | 31 ++++------ .../layout/header/LoggedInHeaderActions.tsx | 34 ++++------- src/sections/session/SessionProvider.tsx | 60 ++----------------- 5 files changed, 32 insertions(+), 103 deletions(-) diff --git a/package-lock.json b/package-lock.json index 49c93c2c9..7fe8ca078 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-pr201.68eefd3", + "@iqss/dataverse-client-javascript": "2.0.0-pr201.c64af18", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -3675,9 +3675,9 @@ }, "node_modules/@iqss/dataverse-client-javascript": { "name": "@IQSS/dataverse-client-javascript", - "version": "2.0.0-pr201.68eefd3", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr201.68eefd3/7a5ae54766cb8aef9e1148eae677963f0cb7cc62", - "integrity": "sha512-OtyZu97M9VfHJbP7FacPNm6YYNGrtUq520KmXU/qRBpIT25ocAnkdIlA3InlUXixf+hROWYM8aa/4vi24TC4BA==", + "version": "2.0.0-pr201.c64af18", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr201.c64af18/7713e5dfc2f1f9c1a1b1095d03838744f21e2747", + "integrity": "sha512-muG2l/xQL62x5BndMae/b2+5EQQHa7n617VVazuQFCjDzZM2rJOf4dafLrmfd6+8hTq9CRfaqe9zV2h9H871XQ==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", diff --git a/src/App.tsx b/src/App.tsx index 89eda7221..25312508d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,8 +32,8 @@ const authConfig: TAuthConfig = { } const userRepository = new UserJSDataverseRepository() + function App() { - console.log({ authConfig }) return ( diff --git a/src/sections/layout/header/Header.tsx b/src/sections/layout/header/Header.tsx index b17ab166e..47389ab58 100644 --- a/src/sections/layout/header/Header.tsx +++ b/src/sections/layout/header/Header.tsx @@ -1,23 +1,23 @@ import { useContext } from 'react' import { AuthContext } from 'react-oauth2-code-pkce' -import dataverse_logo from '../../../assets/dataverse_brand_icon.svg' +import { useLocation } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { Button, Navbar } from '@iqss/dataverse-design-system' -import { Route } from '../../Route.enum' -// import { DATAVERSE_BACKEND_URL } from '../../../config' +import dataverse_logo from '@/assets/dataverse_brand_icon.svg' +import { Route } from '@/sections/Route.enum' +import { useSession } from '@/sections/session/SessionContext' import { LoggedInHeaderActions } from './LoggedInHeaderActions' -import { CollectionJSDataverseRepository } from '../../../collection/infrastructure/repositories/CollectionJSDataverseRepository' +import { CollectionJSDataverseRepository } from '@/collection/infrastructure/repositories/CollectionJSDataverseRepository' import styles from './Header.module.scss' -import { useLocation } from 'react-router-dom' const collectionRepository = new CollectionJSDataverseRepository() export function Header() { const { t } = useTranslation('header') + const { user } = useSession() const { pathname } = useLocation() - // TODO:ME tokenData is originally typed as Record but we know it has a name property (this will need a double check in future iterations) - const { token, tokenData, logIn: oidcLogin } = useContext(AuthContext) + const { logIn: oidcLogin } = useContext(AuthContext) const handleOidcLogIn = () => { oidcLogin(encodeURIComponent(pathname)) @@ -31,19 +31,12 @@ export function Header() { logoImgSrc: dataverse_logo }} className={styles.navbar}> - {token && tokenData ? ( - + {user ? ( + ) : ( - <> - - {/* {t('logIn')} */} - {/* {t('signUp')} */} - + )} ) diff --git a/src/sections/layout/header/LoggedInHeaderActions.tsx b/src/sections/layout/header/LoggedInHeaderActions.tsx index b7b8a762f..1a8db1fd3 100644 --- a/src/sections/layout/header/LoggedInHeaderActions.tsx +++ b/src/sections/layout/header/LoggedInHeaderActions.tsx @@ -1,29 +1,25 @@ import { useContext } from 'react' import { AuthContext } from 'react-oauth2-code-pkce' import { useTranslation } from 'react-i18next' -import { Link, useNavigate } from 'react-router-dom' +import { Link } from 'react-router-dom' import { Navbar } from '@iqss/dataverse-design-system' -import { useGetCollectionUserPermissions } from '../../../shared/hooks/useGetCollectionUserPermissions' -import { useSession } from '../../session/SessionContext' -import { RouteWithParams, Route } from '../../Route.enum' -import { CollectionRepository } from '../../../collection/domain/repositories/CollectionRepository' -import { ROOT_COLLECTION_ALIAS } from '../../../collection/domain/models/Collection' -import { AccountHelper } from '../../account/AccountHelper' - -const currentPage = 0 +import { User } from '@/users/domain/models/User' +import { useGetCollectionUserPermissions } from '@/shared/hooks/useGetCollectionUserPermissions' +import { RouteWithParams, Route } from '@/sections//Route.enum' +import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' +import { ROOT_COLLECTION_ALIAS } from '@/collection/domain/models/Collection' +import { AccountHelper } from '@/sections/account/AccountHelper' interface LoggedInHeaderActionsProps { - userName: string + user: User collectionRepository: CollectionRepository } export const LoggedInHeaderActions = ({ - userName, + user, collectionRepository }: LoggedInHeaderActionsProps) => { const { t } = useTranslation('header') - const { logout } = useSession() - const navigate = useNavigate() const { logOut: oidcLogout } = useContext(AuthContext) @@ -32,17 +28,10 @@ export const LoggedInHeaderActions = ({ collectionRepository: collectionRepository }) - // Just keeping logOut in a handler function because we might want to add more logic here (e.g: logOut can receive up to 3 parameters) const handleOidcLogout = () => { oidcLogout() } - const _handleSessionLogout = () => { - void logout().then(() => { - navigate(currentPage) - }) - } - const createCollectionRoute = RouteWithParams.CREATE_COLLECTION() const createDatasetRoute = RouteWithParams.CREATE_DATASET() @@ -62,7 +51,7 @@ export const LoggedInHeaderActions = ({ {t('navigation.newDataset')}
- + @@ -71,9 +60,6 @@ export const LoggedInHeaderActions = ({ OIDC {t('logOut')} - {/* - {t('logOut')} - */} ) diff --git a/src/sections/session/SessionProvider.tsx b/src/sections/session/SessionProvider.tsx index 008d1ae90..08a360cbc 100644 --- a/src/sections/session/SessionProvider.tsx +++ b/src/sections/session/SessionProvider.tsx @@ -6,66 +6,14 @@ import { getUser } from '../../users/domain/useCases/getUser' import { UserRepository } from '../../users/domain/repositories/UserRepository' import { logOut } from '../../users/domain/useCases/logOut' -const BACKEND_URL = import.meta.env.VITE_DATAVERSE_BACKEND_URL as string - interface SessionProviderProps { repository: UserRepository } export function SessionProvider({ repository, children }: PropsWithChildren) { - const { token, tokenData } = useContext(AuthContext) + const { token, loginInProgress } = useContext(AuthContext) const [user, setUser] = useState(null) const [isLoadingUser, setIsLoadingUser] = useState(true) - useEffect(() => { - // Just to log some data from the AuthContext - console.log( - '%cToken from AuthContext: ', - 'background: green; color: white; padding: 2px; border-radius: 2px;', - { - token - } - ) - - console.log( - '%cTokenData from AuthContext: ', - 'background: green; color: white; padding: 2px; border-radius: 2px;', - tokenData - ) - }, [token, tokenData]) - - useEffect(() => { - if (token) { - fetch(`${BACKEND_URL}/api/v1/users/:me`, { - method: 'GET', - credentials: 'omit', // to avoid sending the cookie - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json' - // 👇 And this throws BAD API key because its a token not an api key of course - // 'X-Dataverse-key': token - } - }) - .then((response) => { - if (!response.ok) { - return response.json().then((errData: Error) => { - throw new Error(`Error ${response.status}: ${errData.message || response.statusText}`) - }) - } - return response.json() - }) - .then((data) => { - console.log('User data:', data) - }) - .catch((error) => { - console.error( - '%cError getting user data with users/:me endpoint', - 'background: #eb5656; color: white; padding: 2px', - error - ) - }) - } - }, [token]) - useEffect(() => { const handleGetUser = async () => { setIsLoadingUser(true) @@ -80,8 +28,10 @@ export function SessionProvider({ repository, children }: PropsWithChildren { return logOut(repository) From bdc76d4045bb9fdeb638de54f36093a437f50a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 14 Oct 2024 15:43:20 -0300 Subject: [PATCH 20/97] feat: fix lint and change wording --- src/sections/layout/header/Header.module.scss | 2 +- src/sections/layout/header/Header.tsx | 2 +- src/sections/layout/header/LoggedInHeaderActions.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sections/layout/header/Header.module.scss b/src/sections/layout/header/Header.module.scss index 72c4a39eb..009b2e3ce 100644 --- a/src/sections/layout/header/Header.module.scss +++ b/src/sections/layout/header/Header.module.scss @@ -4,7 +4,7 @@ box-shadow: 0 1px 5px rgba(0 0 0 / 10%); .login-btn { - color: $dv-subtext-color; + color: var(--bs-nav-link-color); text-decoration: none; } } diff --git a/src/sections/layout/header/Header.tsx b/src/sections/layout/header/Header.tsx index 47389ab58..7538a7b60 100644 --- a/src/sections/layout/header/Header.tsx +++ b/src/sections/layout/header/Header.tsx @@ -35,7 +35,7 @@ export function Header() { ) : ( )} diff --git a/src/sections/layout/header/LoggedInHeaderActions.tsx b/src/sections/layout/header/LoggedInHeaderActions.tsx index 1a8db1fd3..2f47f6d87 100644 --- a/src/sections/layout/header/LoggedInHeaderActions.tsx +++ b/src/sections/layout/header/LoggedInHeaderActions.tsx @@ -58,7 +58,7 @@ export const LoggedInHeaderActions = ({ {t('navigation.apiToken')} - OIDC {t('logOut')} + {t('logOut')} From 882fbb9050be1ebfd9d36b3e0a15ec55132874d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 15 Oct 2024 10:07:09 -0300 Subject: [PATCH 21/97] feat: send login state as object with returnTo path --- src/router/ProtectedRoute.tsx | 9 ++++--- src/sections/auth-callback/AuthCallback.tsx | 29 ++++++++++++++++++++- src/sections/layout/header/Header.tsx | 7 +++-- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/router/ProtectedRoute.tsx b/src/router/ProtectedRoute.tsx index 799184a43..974604683 100644 --- a/src/router/ProtectedRoute.tsx +++ b/src/router/ProtectedRoute.tsx @@ -2,6 +2,7 @@ import { useContext, useEffect } from 'react' import { Outlet, useLocation } from 'react-router-dom' import { AuthContext } from 'react-oauth2-code-pkce' import { AppLoader } from '../sections/shared/layout/app-loader/AppLoader' +import { encodeReturnToPathInStateQueryParam } from '@/sections/auth-callback/AuthCallback' /** * This component is responsible for protecting routes that require authentication. @@ -10,16 +11,18 @@ import { AppLoader } from '../sections/shared/layout/app-loader/AppLoader' */ export const ProtectedRoute = () => { - const { pathname } = useLocation() + const { pathname, search } = useLocation() const { token, loginInProgress, logIn: oidcLogin } = useContext(AuthContext) useEffect(() => { if (loginInProgress) return if (!token) { - oidcLogin(encodeURIComponent(pathname)) + const state = encodeReturnToPathInStateQueryParam(`${pathname}${search}`) + + oidcLogin(state) } - }, [token, oidcLogin, pathname, loginInProgress]) + }, [token, oidcLogin, pathname, loginInProgress, search]) if (loginInProgress) { return diff --git a/src/sections/auth-callback/AuthCallback.tsx b/src/sections/auth-callback/AuthCallback.tsx index 66f03cb7e..5f3b0862a 100644 --- a/src/sections/auth-callback/AuthCallback.tsx +++ b/src/sections/auth-callback/AuthCallback.tsx @@ -4,6 +4,8 @@ import { AuthContext } from 'react-oauth2-code-pkce' import { QueryParamKey } from '../Route.enum' import { AppLoader } from '../shared/layout/app-loader/AppLoader' +export type AuthStateQueryParamValue = { returnTo: string } + /** * This component will we rendered as redirectUri page after the OIDC login is complete. * It will redirect the user to the intended page before the OIDC login was initiated. @@ -25,8 +27,33 @@ export const AuthCallback = () => { return } - navigate(decodeURIComponent(stateQueryParam), { replace: true }) + const returnToPath = decodeReturnToPathFromStateQueryParam(stateQueryParam) + + navigate(returnToPath, { replace: true }) }, [stateQueryParam, navigate, loginInProgress]) return } + +export const encodeReturnToPathInStateQueryParam = (returnToPath: string): string => { + const returnToObject: AuthStateQueryParamValue = { returnTo: returnToPath } + + return encodeURIComponent(JSON.stringify(returnToObject)) +} + +export const decodeReturnToPathFromStateQueryParam = (stateQueryParam: string): string => { + const decodedStateQueryParam = decodeURIComponent(stateQueryParam) + const parsedStateQueryParam = JSON.parse(decodedStateQueryParam) as unknown + + if (isReturnToObject(parsedStateQueryParam)) { + return parsedStateQueryParam.returnTo + } + + return '/' +} + +function isReturnToObject(obj: unknown): obj is AuthStateQueryParamValue { + return ( + obj !== null && typeof obj === 'object' && 'returnTo' in obj && typeof obj.returnTo === 'string' + ) +} diff --git a/src/sections/layout/header/Header.tsx b/src/sections/layout/header/Header.tsx index 7538a7b60..e39a50a12 100644 --- a/src/sections/layout/header/Header.tsx +++ b/src/sections/layout/header/Header.tsx @@ -8,6 +8,7 @@ import { Route } from '@/sections/Route.enum' import { useSession } from '@/sections/session/SessionContext' import { LoggedInHeaderActions } from './LoggedInHeaderActions' import { CollectionJSDataverseRepository } from '@/collection/infrastructure/repositories/CollectionJSDataverseRepository' +import { encodeReturnToPathInStateQueryParam } from '@/sections/auth-callback/AuthCallback' import styles from './Header.module.scss' const collectionRepository = new CollectionJSDataverseRepository() @@ -15,12 +16,14 @@ const collectionRepository = new CollectionJSDataverseRepository() export function Header() { const { t } = useTranslation('header') const { user } = useSession() - const { pathname } = useLocation() + const { pathname, search } = useLocation() const { logIn: oidcLogin } = useContext(AuthContext) const handleOidcLogIn = () => { - oidcLogin(encodeURIComponent(pathname)) + const state = encodeReturnToPathInStateQueryParam(`${pathname}${search}`) + + oidcLogin(state) } return ( From 5fba27f8730f3f5a0bafe6f32f12cff6afc5ad7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 25 Oct 2024 10:12:01 -0300 Subject: [PATCH 22/97] chore: bump package version --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7fe8ca078..573e751b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "react-infinite-scroll-hook": "4.1.1", "react-loader-spinner": "5.3.4", "react-markdown": "8.0.7", - "react-oauth2-code-pkce": "1.20.2", + "react-oauth2-code-pkce": "1.22.1", "react-router-dom": "6.23.1", "react-topbar-progress-indicator": "4.1.1", "sass": "1.58.1", @@ -35899,9 +35899,9 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/react-oauth2-code-pkce": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/react-oauth2-code-pkce/-/react-oauth2-code-pkce-1.20.2.tgz", - "integrity": "sha512-ZnGX7I4opkTdXK3OXY7fIJTTSia3wy/pmMf2a/TPcQ8ZlYhNe5HiOf2DYOcpqROQttmmKaZ8OnNd28A/Umv5ew==", + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/react-oauth2-code-pkce/-/react-oauth2-code-pkce-1.22.1.tgz", + "integrity": "sha512-HJibHs5p2HnaO8u86cKaEDg5bZ2VzdNM+nhsL6PlUmEqqgFM6uZPgp7OH2CXpCtJS7mR3nTNpYNTOnjqS/MZjw==", "peerDependencies": { "react": ">=16.8.0" } diff --git a/package.json b/package.json index 87c04ea75..b7e62ea08 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "react-markdown": "8.0.7", "react-router-dom": "6.23.1", "react-topbar-progress-indicator": "4.1.1", - "react-oauth2-code-pkce": "1.20.2", + "react-oauth2-code-pkce": "1.22.1", "sass": "1.58.1", "typescript": "4.9.5", "use-deep-compare": "1.2.1", From dc42ba964d17ecd592835cd6aa5431e4ab8224fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 29 Oct 2024 11:08:56 -0300 Subject: [PATCH 23/97] feat: navigate back to parent collection --- .../create-collection/collection-form/CollectionForm.tsx | 8 ++++---- .../form/DatasetMetadataForm/MetadataForm/index.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/sections/create-collection/collection-form/CollectionForm.tsx b/src/sections/create-collection/collection-form/CollectionForm.tsx index 43d9c3b39..7296dbfcd 100644 --- a/src/sections/create-collection/collection-form/CollectionForm.tsx +++ b/src/sections/create-collection/collection-form/CollectionForm.tsx @@ -1,4 +1,4 @@ -import { MouseEvent, useMemo, useRef } from 'react' +import { useMemo, useRef } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' @@ -18,6 +18,7 @@ import { SeparationLine } from '../../shared/layout/SeparationLine/SeparationLin import { TopFieldsSection } from './top-fields-section/TopFieldsSection' import { MetadataFieldsSection } from './metadata-fields-section/MetadataFieldsSection' import { BrowseSearchFacetsSection } from './browse-search-facets-section/BrowseSearchFacetsSection' +import { RouteWithParams } from '@/sections/Route.enum' import styles from './CollectionForm.module.scss' export const METADATA_BLOCKS_NAMES_GROUPER = 'metadataBlockNames' @@ -132,9 +133,8 @@ export const CollectionForm = ({ } } - const handleCancel = (event: MouseEvent) => { - event.preventDefault() - navigate(-1) + const handleCancel = () => { + navigate(RouteWithParams.COLLECTIONS(ownerCollectionId)) } const disableSubmitButton = useMemo(() => { diff --git a/src/sections/shared/form/DatasetMetadataForm/MetadataForm/index.tsx b/src/sections/shared/form/DatasetMetadataForm/MetadataForm/index.tsx index 049d7a276..8a7371639 100644 --- a/src/sections/shared/form/DatasetMetadataForm/MetadataForm/index.tsx +++ b/src/sections/shared/form/DatasetMetadataForm/MetadataForm/index.tsx @@ -1,4 +1,4 @@ -import { MouseEvent, useEffect, useMemo, useRef } from 'react' +import { useEffect, useMemo, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { FieldErrors, FormProvider, useForm } from 'react-hook-form' @@ -12,6 +12,7 @@ import { SubmissionStatus, useSubmitDataset } from '../useSubmitDataset' import { MetadataBlockFormFields } from './MetadataBlockFormFields' import { RequiredFieldText } from '../../RequiredFieldText/RequiredFieldText' import { SeparationLine } from '../../../layout/SeparationLine/SeparationLine' +import { RouteWithParams } from '@/sections/Route.enum' import styles from './index.module.scss' interface FormProps { @@ -71,9 +72,8 @@ export const MetadataForm = ({ } }, [setValue, user, mode]) - const handleCancel = (event: MouseEvent) => { - event.preventDefault() - navigate(-1) + const handleCancel = () => { + navigate(RouteWithParams.COLLECTIONS(collectionId)) } const onInvalidSubmit = (errors: FieldErrors) => { From 06cb72169f6f118eee11f458c829a313b0e12171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 29 Oct 2024 11:09:22 -0300 Subject: [PATCH 24/97] feat: remove unused refreshTokenExpire --- src/App.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 25312508d..82a418cc5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { AuthProvider, TAuthConfig, TRefreshTokenExpiredEvent } from 'react-oauth2-code-pkce' +import { AuthProvider, TAuthConfig } from 'react-oauth2-code-pkce' import { ApiConfig } from '@iqss/dataverse-client-javascript/dist/core' import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' import { Router } from './router' @@ -25,8 +25,6 @@ const authConfig: TAuthConfig = { logoutRedirect: `${origin}${BASENAME_URL}`, redirectUri: `${origin}${BASENAME_URL}${Route.AUTH_CALLBACK}`, scope: 'openid', - onRefreshTokenExpire: (event: TRefreshTokenExpiredEvent) => - event.logIn(undefined, undefined, 'popup'), autoLogin: false, clearURL: false } From 1f1b0cf78918932d8a405a0cdc550f64edcee11d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 29 Oct 2024 11:11:04 -0300 Subject: [PATCH 25/97] feat: protected route dont render outlet unless user is set --- src/router/ProtectedRoute.tsx | 12 ++++++++---- src/sections/create-dataset/CreateDataset.tsx | 4 ++-- src/sections/layout/header/Header.tsx | 6 +++++- src/sections/layout/header/LoggedInHeaderActions.tsx | 2 +- src/sections/session/SessionProvider.tsx | 4 ++-- src/users/domain/useCases/getUser.ts | 2 +- 6 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/router/ProtectedRoute.tsx b/src/router/ProtectedRoute.tsx index 974604683..1a334f0ec 100644 --- a/src/router/ProtectedRoute.tsx +++ b/src/router/ProtectedRoute.tsx @@ -3,6 +3,7 @@ import { Outlet, useLocation } from 'react-router-dom' import { AuthContext } from 'react-oauth2-code-pkce' import { AppLoader } from '../sections/shared/layout/app-loader/AppLoader' import { encodeReturnToPathInStateQueryParam } from '@/sections/auth-callback/AuthCallback' +import { useSession } from '@/sections/session/SessionContext' /** * This component is responsible for protecting routes that require authentication. @@ -12,19 +13,22 @@ import { encodeReturnToPathInStateQueryParam } from '@/sections/auth-callback/Au export const ProtectedRoute = () => { const { pathname, search } = useLocation() - const { token, loginInProgress, logIn: oidcLogin } = useContext(AuthContext) + const { token, loginInProgress: oidcLoginInProgress, logIn: oidcLogin } = useContext(AuthContext) + const { user, isLoadingUser } = useSession() + + const isSafeToRenderProtectedRoute = !oidcLoginInProgress && !isLoadingUser && token && user useEffect(() => { - if (loginInProgress) return + if (oidcLoginInProgress || isLoadingUser) return if (!token) { const state = encodeReturnToPathInStateQueryParam(`${pathname}${search}`) oidcLogin(state) } - }, [token, oidcLogin, pathname, loginInProgress, search]) + }, [token, oidcLogin, oidcLoginInProgress, isLoadingUser, pathname, search]) - if (loginInProgress) { + if (!isSafeToRenderProtectedRoute) { return } diff --git a/src/sections/create-dataset/CreateDataset.tsx b/src/sections/create-dataset/CreateDataset.tsx index a7862eb83..a040f3b1d 100644 --- a/src/sections/create-dataset/CreateDataset.tsx +++ b/src/sections/create-dataset/CreateDataset.tsx @@ -74,7 +74,7 @@ export function CreateDataset({ return ( <> -
+
-
+ ) } diff --git a/src/sections/layout/header/Header.tsx b/src/sections/layout/header/Header.tsx index e39a50a12..652c3f6d0 100644 --- a/src/sections/layout/header/Header.tsx +++ b/src/sections/layout/header/Header.tsx @@ -37,7 +37,11 @@ export function Header() { {user ? ( ) : ( - )} diff --git a/src/sections/layout/header/LoggedInHeaderActions.tsx b/src/sections/layout/header/LoggedInHeaderActions.tsx index 2f47f6d87..fbaa74fbe 100644 --- a/src/sections/layout/header/LoggedInHeaderActions.tsx +++ b/src/sections/layout/header/LoggedInHeaderActions.tsx @@ -57,7 +57,7 @@ export const LoggedInHeaderActions = ({ to={`${Route.ACCOUNT}?${AccountHelper.ACCOUNT_PANEL_TAB_QUERY_KEY}=${AccountHelper.ACCOUNT_PANEL_TABS_KEYS.apiToken}`}> {t('navigation.apiToken')} - + {t('logOut')} diff --git a/src/sections/session/SessionProvider.tsx b/src/sections/session/SessionProvider.tsx index 08a360cbc..b53b71ecc 100644 --- a/src/sections/session/SessionProvider.tsx +++ b/src/sections/session/SessionProvider.tsx @@ -12,13 +12,13 @@ interface SessionProviderProps { export function SessionProvider({ repository, children }: PropsWithChildren) { const { token, loginInProgress } = useContext(AuthContext) const [user, setUser] = useState(null) - const [isLoadingUser, setIsLoadingUser] = useState(true) + const [isLoadingUser, setIsLoadingUser] = useState(false) useEffect(() => { const handleGetUser = async () => { setIsLoadingUser(true) try { - const user: User | void = await getUser(repository) + const user: User = await getUser(repository) user && setUser(user) } catch (error) { diff --git a/src/users/domain/useCases/getUser.ts b/src/users/domain/useCases/getUser.ts index a415fad81..40aae3d7e 100644 --- a/src/users/domain/useCases/getUser.ts +++ b/src/users/domain/useCases/getUser.ts @@ -1,6 +1,6 @@ import { User } from '../models/User' import { UserRepository } from '../repositories/UserRepository' -export function getUser(userRepository: UserRepository): Promise { +export function getUser(userRepository: UserRepository): Promise { return userRepository.getAuthenticated() } From 666a71ebdcd7f945f0d370bec8ab0de3ffaf2633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 29 Oct 2024 11:12:29 -0300 Subject: [PATCH 26/97] test: initial e2e test work with new auth mechanism --- .../sections/collection/Collection.spec.ts | 15 +----- .../CreateCollection.spec.tsx | 43 +++++++++++++---- .../create-dataset/CreateDataset.spec.tsx | 40 ++++++++++------ .../shared/DataverseApiHelper.ts | 48 ++++++++++++++----- tests/e2e-integration/shared/TestsUtils.ts | 13 +++-- tests/support/commands.tsx | 29 +++++------ tests/support/component.ts | 1 + 7 files changed, 117 insertions(+), 72 deletions(-) diff --git a/tests/e2e-integration/e2e/sections/collection/Collection.spec.ts b/tests/e2e-integration/e2e/sections/collection/Collection.spec.ts index 8af6e0b6f..4bd7e6d3c 100644 --- a/tests/e2e-integration/e2e/sections/collection/Collection.spec.ts +++ b/tests/e2e-integration/e2e/sections/collection/Collection.spec.ts @@ -5,13 +5,9 @@ import { CollectionHelper } from '../../../shared/collection/CollectionHelper' describe('Collection Page', () => { const title = faker.lorem.sentence() - before(() => { - TestsUtils.setup() - TestsUtils.login() - }) - beforeEach(() => { TestsUtils.login() + TestsUtils.setup() }) it('successfully loads root collection when accessing the home', () => { @@ -73,15 +69,6 @@ describe('Collection Page', () => { .should('exist') }) - it('log out Dataverse Admin user', () => { - cy.visit('/spa/collections') - cy.findAllByText(/Root/i).should('exist') - - cy.findByText(/Dataverse Admin/i).click() - cy.findByRole('button', { name: /Log Out/i }).click() - cy.findByText(/Dataverse Admin/i).should('not.exist') - }) - describe.skip('Currently skipping all tests as we are only rendering an infinite scrollable container. Please refactor these tests if a toggle button is added to switch between pagination and infinite scroll.', () => { it('navigates to the correct page of the datasets list when passing the page query param', () => { cy.wrap(DatasetHelper.createMany(12), { timeout: 10000 }).then(() => { diff --git a/tests/e2e-integration/e2e/sections/create-collection/CreateCollection.spec.tsx b/tests/e2e-integration/e2e/sections/create-collection/CreateCollection.spec.tsx index 0fc42fe36..c0a22da7f 100644 --- a/tests/e2e-integration/e2e/sections/create-collection/CreateCollection.spec.tsx +++ b/tests/e2e-integration/e2e/sections/create-collection/CreateCollection.spec.tsx @@ -1,17 +1,22 @@ import { TestsUtils } from '../../../shared/TestsUtils' import { faker } from '@faker-js/faker' +const CREATE_COLLECTION_PAGE_URL = '/spa/collections/root/create' + describe('Create Collection', () => { - before(() => { + beforeEach(() => { + TestsUtils.login() TestsUtils.setup() }) - beforeEach(() => { - TestsUtils.login() + it('visits the Create Collection Page as a logged in user', () => { + cy.visit(CREATE_COLLECTION_PAGE_URL) + + cy.findByRole('heading', { name: 'Create Collection' }).should('exist') }) it('navigates to the collection page after submitting a valid form', () => { - cy.visit('/spa/collections/root/create') + cy.visit(CREATE_COLLECTION_PAGE_URL) const collectionName = faker.lorem.words(3) @@ -29,7 +34,7 @@ describe('Create Collection', () => { }) it('shows correct selected facets from parent collection in Browse/Search facets section', () => { - cy.visit('/spa/collections/root/create') + cy.visit(CREATE_COLLECTION_PAGE_URL) const collectionName = faker.lorem.words(3) @@ -87,11 +92,29 @@ describe('Create Collection', () => { }) }) - it('redirects to the Log in page when the user is not authenticated', () => { - cy.wrap(TestsUtils.logout()) + it('should redirect the user to the Login page when the user is not authenticated', () => { + TestsUtils.logout() + + // Visit a protected route 🔐, ProtectedRoute component should redirect automatically to the Keycloack login page + cy.visit(CREATE_COLLECTION_PAGE_URL) + + // Check if the Keycloak login form is present + cy.get('#kc-form-login').should('exist') + }) + + it('should redirect the user back to the create collection page after a successful login', () => { + TestsUtils.logout() + + cy.visit(CREATE_COLLECTION_PAGE_URL) + + // Check if the Keycloak login form is present + cy.get('#kc-form-login').should('exist') + + TestsUtils.enterCredentialsInKeycloak() + + // Check if the user is redirected back to the create collection page + cy.url().should('eq', `${Cypress.config().baseUrl as string}${CREATE_COLLECTION_PAGE_URL}`) - cy.visit('/spa/collections/root/create') - cy.get('#login-container').should('exist') - cy.url().should('include', '/loginpage.xhtml') + cy.findByRole('heading', { name: 'Create Collection' }).should('exist') }) }) diff --git a/tests/e2e-integration/e2e/sections/create-dataset/CreateDataset.spec.tsx b/tests/e2e-integration/e2e/sections/create-dataset/CreateDataset.spec.tsx index 430dece32..a31b2c112 100644 --- a/tests/e2e-integration/e2e/sections/create-dataset/CreateDataset.spec.tsx +++ b/tests/e2e-integration/e2e/sections/create-dataset/CreateDataset.spec.tsx @@ -1,23 +1,22 @@ import { TestsUtils } from '../../../shared/TestsUtils' import { DatasetLabelValue } from '../../../../../src/dataset/domain/models/Dataset' -describe('Create Dataset', () => { - before(() => { - TestsUtils.setup() - }) +const CREATE_DATASET_PAGE_URL = '/spa/datasets/root/create' +describe('Create Dataset', () => { beforeEach(() => { TestsUtils.login() + TestsUtils.setup() }) it('visits the Create Dataset Page as a logged in user', () => { - cy.visit('/spa/datasets/root/create') + cy.visit(CREATE_DATASET_PAGE_URL) cy.findByRole('heading', { name: 'Create Dataset' }).should('exist') }) it('navigates to the new dataset after submitting a valid form', () => { - cy.visit('/spa/datasets/root/create') + cy.visit(CREATE_DATASET_PAGE_URL) cy.findByLabelText(/^Title/i).type('Test Dataset Title', { force: true }) @@ -43,19 +42,30 @@ describe('Create Dataset', () => { cy.findByText(DatasetLabelValue.UNPUBLISHED).should('exist') }) - it('navigates to the home if the user cancels the form', () => { - cy.visit('/spa/datasets/root/create') + it('should redirect the user to the Login page when the user is not authenticated', () => { + TestsUtils.logout() - cy.findByText(/Cancel/i).click() + // Visit a protected route 🔐, ProtectedRoute component should redirect automatically to the Keycloack login page + cy.visit(CREATE_DATASET_PAGE_URL) - cy.findByRole('heading', { name: 'Root' }).should('exist') + // Check if the Keycloak login form is present + cy.get('#kc-form-login').should('exist') }) - it('redirects to the Log In page when the user is not authenticated', () => { - cy.wrap(TestsUtils.logout()) + it('should redirect the user back to the create dataset page after a successful login', () => { + TestsUtils.logout() - cy.visit('/spa/datasets/root/create') - cy.get('#login-container').should('exist') - cy.url().should('include', '/loginpage.xhtml') + cy.visit(CREATE_DATASET_PAGE_URL) + + // Check if the Keycloak login form is present + cy.get('#kc-form-login').should('exist') + + // Enter the credentials in the Keycloak login form + TestsUtils.enterCredentialsInKeycloak() + + // Check if the user is redirected back to the create dataset page + cy.url().should('eq', `${Cypress.config().baseUrl as string}${CREATE_DATASET_PAGE_URL}`) + + cy.findByRole('heading', { name: 'Create Dataset' }).should('exist') }) }) diff --git a/tests/e2e-integration/shared/DataverseApiHelper.ts b/tests/e2e-integration/shared/DataverseApiHelper.ts index 6473ac937..2bf22883e 100644 --- a/tests/e2e-integration/shared/DataverseApiHelper.ts +++ b/tests/e2e-integration/shared/DataverseApiHelper.ts @@ -7,16 +7,17 @@ export class DataverseApiHelper { static setup() { this.API_URL = `${TestsUtils.DATAVERSE_BACKEND_URL}/api` - // TODO - Replace with an ajax call to the API - cy.getApiToken().then((token) => { - this.API_TOKEN = token - }) - void this.request('/admin/settings/:MaxEmbargoDurationInMonths', 'PUT', -1) - void this.request( - '/admin/settings/:AnonymizedFieldTypeNames', - 'PUT', - 'author, datasetContact, contributor, depositor, grantNumber, publication' - ) + const token = this.getLocalStorageItem('ROCP_token') + + if (token) { + console.log('Setting embargo and anonymized field types') + void this.request('/admin/settings/:MaxEmbargoDurationInMonths', 'PUT', -1) + void this.request( + '/admin/settings/:AnonymizedFieldTypeNames', + 'PUT', + 'author, datasetContact, contributor, depositor, grantNumber, publication' + ) + } } static async request( @@ -27,17 +28,30 @@ export class DataverseApiHelper { contentType = 'application/json' ): Promise { const isFormData = contentType === 'multipart/form-data' + const config: AxiosRequestConfig = { url: `${this.API_URL}${url}`, method: method, headers: { - 'X-Dataverse-key': this.API_TOKEN, 'Content-Type': contentType }, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - data: isFormData ? this.createFormData(data) : data + data: isFormData ? this.createFormData(data) : data, + withCredentials: false } + // Intercept the request to add the token + axios.interceptors.request.use((config) => { + const token = this.getLocalStorageItem('ROCP_token') + + console.log('%cRequest with Token:', 'background: green; color: white;', token) + + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }) + const response: { data: { data: T } } = await axios(config) return response.data.data } @@ -63,4 +77,14 @@ export class DataverseApiHelper { return formData } + + static getLocalStorageItem(key: string): T | null { + try { + const item = localStorage.getItem(key) + return item ? (JSON.parse(item) as T) : null + } catch (error) { + console.error(`Error parsing localStorage key "${key}":`, error) + return null + } + } } diff --git a/tests/e2e-integration/shared/TestsUtils.ts b/tests/e2e-integration/shared/TestsUtils.ts index 78ee00b96..5dd0ad702 100644 --- a/tests/e2e-integration/shared/TestsUtils.ts +++ b/tests/e2e-integration/shared/TestsUtils.ts @@ -1,7 +1,6 @@ import { ApiConfig } from '@iqss/dataverse-client-javascript/dist/core' import { DataverseApiHelper } from './DataverseApiHelper' import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' -import { UserJSDataverseRepository } from '../../../src/users/infrastructure/repositories/UserJSDataverseRepository' import { DatasetHelper } from './datasets/DatasetHelper' import { DATAVERSE_BACKEND_URL } from '../../../src/config' @@ -9,12 +8,18 @@ export class TestsUtils { static readonly DATAVERSE_BACKEND_URL = DATAVERSE_BACKEND_URL static setup() { - ApiConfig.init(`${this.DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.SESSION_COOKIE) + ApiConfig.init(`${this.DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) DataverseApiHelper.setup() } static login() { - return cy.loginAsAdmin() // TODO - Replace with an ajax call to the API + return cy.loginAsAdmin() + } + + static enterCredentialsInKeycloak() { + cy.get('#username').type('dataverse-admin@mailinator.com') + cy.get('#password').type('admin') + cy.get('#kc-login').click() } static wait(ms: number): Promise { @@ -24,7 +29,7 @@ export class TestsUtils { } static logout() { - return new UserJSDataverseRepository().removeAuthenticated() + return cy.logout() } static async waitForNoLocks(persistentId: string, maxRetries = 20, delay = 1000): Promise { diff --git a/tests/support/commands.tsx b/tests/support/commands.tsx index 87a412ac2..726c85b83 100644 --- a/tests/support/commands.tsx +++ b/tests/support/commands.tsx @@ -46,6 +46,7 @@ import i18next from '../../src/i18n' import { UserRepository } from '../../src/users/domain/repositories/UserRepository' import { SessionProvider } from '../../src/sections/session/SessionProvider' import { MemoryRouter } from 'react-router-dom' +import { TestsUtils } from '@tests/e2e-integration/shared/TestsUtils' // Define your custom mount function @@ -77,25 +78,19 @@ Cypress.Commands.add('mountSuperuser', (component: ReactNode) => { return cy.customMount({component}) }) -Cypress.Commands.add('loginAsAdmin', (go?: string) => { - cy.visit('/') - cy.get('#topNavBar').then((navbar) => { - if (navbar.find('ul > li:nth-child(6) > a').text().includes('Log In')) { - cy.findAllByRole('link', { name: /Log In/i }) - .first() - .click() - cy.findByLabelText('Username/Email').type('dataverseAdmin') - cy.findByLabelText('Password').type('admin1') - cy.findByRole('button', { name: /Log In/i }).click() - cy.findAllByText(/Dataverse Admin/i).should('exist') - if (go) cy.visit(go) - } - }) +Cypress.Commands.add('loginAsAdmin', () => { + cy.visit('/spa/') + cy.findByTestId('oidc-login').click() + + TestsUtils.enterCredentialsInKeycloak() + + cy.url().should('eq', `${Cypress.config().baseUrl as string}/spa`) }) -Cypress.Commands.add('getApiToken', () => { - cy.loginAsAdmin('/dataverseuser.xhtml?selectTab=dataRelatedToMe') - return cy.findByRole('link', { name: 'API Token' }).click().get('#apiToken code').invoke('text') +Cypress.Commands.add('logout', () => { + cy.visit('/spa/') + cy.get('#dropdown-user').click() + cy.findByTestId('oidc-logout').click() }) Cypress.Commands.add('compareDate', (date, expectedDate) => { diff --git a/tests/support/component.ts b/tests/support/component.ts index 67c84174e..ad869ebb0 100644 --- a/tests/support/component.ts +++ b/tests/support/component.ts @@ -37,6 +37,7 @@ declare global { mountAuthenticated: typeof mount mountSuperuser: typeof mount loginAsAdmin(go?: string): Chainable> + logout(): Chainable> getApiToken(): Chainable compareDate(date: Date, expectedDate: Date): Chainable } From b75cfc1487ae6935fdda52d33d23494cd9e0e5bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 4 Nov 2024 17:25:10 -0300 Subject: [PATCH 27/97] test: refactor api helpers and utils --- .../shared/DataverseApiHelper.ts | 80 +++++++++++++------ tests/e2e-integration/shared/TestsUtils.ts | 34 +++++--- tests/support/commands.tsx | 10 ++- tests/support/component.ts | 3 +- 4 files changed, 86 insertions(+), 41 deletions(-) diff --git a/tests/e2e-integration/shared/DataverseApiHelper.ts b/tests/e2e-integration/shared/DataverseApiHelper.ts index 2bf22883e..f3918a4c2 100644 --- a/tests/e2e-integration/shared/DataverseApiHelper.ts +++ b/tests/e2e-integration/shared/DataverseApiHelper.ts @@ -5,18 +5,40 @@ export class DataverseApiHelper { private static API_TOKEN = '' private static API_URL = '' - static setup() { - this.API_URL = `${TestsUtils.DATAVERSE_BACKEND_URL}/api` - const token = this.getLocalStorageItem('ROCP_token') + static async setup(bearerToken: string) { + console.log( + '%cSetting up Dataverse API...', + 'background: blue; color: white; padding: 2px; border-radius: 4px;' + ) + + this.API_URL = `${TestsUtils.DATAVERSE_BACKEND_URL}/api/v1` + + try { + const createdApiToken = await this.createAndGetApiKeyWithBearerToken(bearerToken) + + this.API_TOKEN = createdApiToken - if (token) { - console.log('Setting embargo and anonymized field types') void this.request('/admin/settings/:MaxEmbargoDurationInMonths', 'PUT', -1) void this.request( '/admin/settings/:AnonymizedFieldTypeNames', 'PUT', 'author, datasetContact, contributor, depositor, grantNumber, publication' ) + } catch (error) { + console.log( + '%cError setting up Dataverse API', + 'background: red; color: white; padding: 2px; border-radius: 4px;' + ) + console.log(error) + } finally { + console.log( + '%cDataverse API setup complete', + 'background: green; color: white; padding: 2px; border-radius: 4px;' + ) + console.group('Dataverse API setup results') + console.log('API URL:', this.API_URL) + console.log('API Token:', this.API_TOKEN) + console.groupEnd() } } @@ -27,12 +49,18 @@ export class DataverseApiHelper { data?: any, contentType = 'application/json' ): Promise { + console.log( + '%cMaking request...', + 'background: violet; color: white; padding: 2px; border-radius: 4px;' + ) + const isFormData = contentType === 'multipart/form-data' const config: AxiosRequestConfig = { url: `${this.API_URL}${url}`, method: method, headers: { + 'X-Dataverse-key': this.API_TOKEN, 'Content-Type': contentType }, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment @@ -40,18 +68,6 @@ export class DataverseApiHelper { withCredentials: false } - // Intercept the request to add the token - axios.interceptors.request.use((config) => { - const token = this.getLocalStorageItem('ROCP_token') - - console.log('%cRequest with Token:', 'background: green; color: white;', token) - - if (token) { - config.headers.Authorization = `Bearer ${token}` - } - return config - }) - const response: { data: { data: T } } = await axios(config) return response.data.data } @@ -78,13 +94,27 @@ export class DataverseApiHelper { return formData } - static getLocalStorageItem(key: string): T | null { - try { - const item = localStorage.getItem(key) - return item ? (JSON.parse(item) as T) : null - } catch (error) { - console.error(`Error parsing localStorage key "${key}":`, error) - return null - } + static async createAndGetApiKeyWithBearerToken(bearerToken: string): Promise { + console.log( + '%cCreating test API key...', + 'background: blue; color: white; padding: 2px; border-radius: 4px;' + ) + + const { data }: { data: { data: { message: string } } } = await axios.post( + `${this.API_URL}/users/token/recreate`, + {}, + { + headers: { + Authorization: `Bearer ${bearerToken}` + }, + withCredentials: false + } + ) + + const messageParts = data.data.message.split(' ') + + const apiKey = messageParts[5] + + return apiKey } } diff --git a/tests/e2e-integration/shared/TestsUtils.ts b/tests/e2e-integration/shared/TestsUtils.ts index 5dd0ad702..4e6585838 100644 --- a/tests/e2e-integration/shared/TestsUtils.ts +++ b/tests/e2e-integration/shared/TestsUtils.ts @@ -7,19 +7,17 @@ import { DATAVERSE_BACKEND_URL } from '../../../src/config' export class TestsUtils { static readonly DATAVERSE_BACKEND_URL = DATAVERSE_BACKEND_URL - static setup() { - ApiConfig.init(`${this.DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) - DataverseApiHelper.setup() + static async setup(bearerToken: string) { + ApiConfig.init(`${this.DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.API_KEY) + await DataverseApiHelper.setup(bearerToken) } static login() { - return cy.loginAsAdmin() + return cy.login() } - static enterCredentialsInKeycloak() { - cy.get('#username').type('dataverse-admin@mailinator.com') - cy.get('#password').type('admin') - cy.get('#kc-login').click() + static logout() { + return cy.logout() } static wait(ms: number): Promise { @@ -28,10 +26,6 @@ export class TestsUtils { }) } - static logout() { - return cy.logout() - } - static async waitForNoLocks(persistentId: string, maxRetries = 20, delay = 1000): Promise { await this.checkForLocks(persistentId, maxRetries, delay) } @@ -60,4 +54,20 @@ export class TestsUtils { console.log('Max retries reached.') throw new Error('Max retries reached.') } + + static enterCredentialsInKeycloak() { + cy.get('#username').type('dataverse-admin@mailinator.com') + cy.get('#password').type('admin') + cy.get('#kc-login').click() + } + + static getLocalStorageItem(key: string): T | null { + try { + const item = localStorage.getItem(key) + return item ? (JSON.parse(item) as T) : null + } catch (error) { + console.error(`Error parsing localStorage key "${key}":`, error) + return null + } + } } diff --git a/tests/support/commands.tsx b/tests/support/commands.tsx index 726c85b83..5f337e126 100644 --- a/tests/support/commands.tsx +++ b/tests/support/commands.tsx @@ -78,13 +78,19 @@ Cypress.Commands.add('mountSuperuser', (component: ReactNode) => { return cy.customMount({component}) }) -Cypress.Commands.add('loginAsAdmin', () => { +Cypress.Commands.add('login', () => { cy.visit('/spa/') cy.findByTestId('oidc-login').click() TestsUtils.enterCredentialsInKeycloak() - cy.url().should('eq', `${Cypress.config().baseUrl as string}/spa`) + cy.url() + .should('eq', `${Cypress.config().baseUrl as string}/spa`) + .then(() => { + const token = TestsUtils.getLocalStorageItem('ROCP_token') + + return cy.wrap(token) + }) }) Cypress.Commands.add('logout', () => { diff --git a/tests/support/component.ts b/tests/support/component.ts index ad869ebb0..13f20f361 100644 --- a/tests/support/component.ts +++ b/tests/support/component.ts @@ -36,9 +36,8 @@ declare global { customMount: typeof mount mountAuthenticated: typeof mount mountSuperuser: typeof mount - loginAsAdmin(go?: string): Chainable> + login(): Chainable logout(): Chainable> - getApiToken(): Chainable compareDate(date: Date, expectedDate: Date): Chainable } } From da54b6f31672cd5f86f266b179e1d1f67346904e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 4 Nov 2024 17:25:24 -0300 Subject: [PATCH 28/97] test: refactor tests with new oidc --- .../sections/collection/Collection.spec.ts | 33 ++++-------- .../collection/CollectionItemsPanel.spec.ts | 32 +++++++----- .../CreateCollection.spec.tsx | 9 +++- .../create-dataset/CreateDataset.spec.tsx | 9 +++- .../e2e/sections/dataset/Dataset.spec.tsx | 14 +++-- .../EditDatasetMetadata.spec.tsx | 52 +++++++++++++++---- .../e2e/sections/file/File.spec.tsx | 19 ++++--- 7 files changed, 108 insertions(+), 60 deletions(-) diff --git a/tests/e2e-integration/e2e/sections/collection/Collection.spec.ts b/tests/e2e-integration/e2e/sections/collection/Collection.spec.ts index 4bd7e6d3c..b93918f0d 100644 --- a/tests/e2e-integration/e2e/sections/collection/Collection.spec.ts +++ b/tests/e2e-integration/e2e/sections/collection/Collection.spec.ts @@ -5,9 +5,15 @@ import { CollectionHelper } from '../../../shared/collection/CollectionHelper' describe('Collection Page', () => { const title = faker.lorem.sentence() + beforeEach(() => { - TestsUtils.login() - TestsUtils.setup() + TestsUtils.login().then((token) => { + if (!token) { + throw new Error('Token not found after Keycloak login') + } + + cy.wrap(TestsUtils.setup(token)) + }) }) it('successfully loads root collection when accessing the home', () => { @@ -28,6 +34,7 @@ describe('Collection Page', () => { cy.findAllByText(title).should('be.visible') }) }) + it('Successfully publishes a collection', () => { const timestamp = new Date().valueOf() const uniqueCollectionId = `test-publish-collection-${timestamp}` @@ -46,28 +53,6 @@ describe('Collection Page', () => { cy.findByRole('button', { name: 'Publish' }).should('not.exist') }) }) - it('Navigates to Create Dataset page when New Dataset link clicked', () => { - cy.visit('/spa/collections') - - cy.get('nav.navbar').within(() => { - const addDataBtn = cy.findByRole('button', { name: /Add Data/i }) - addDataBtn.should('exist') - addDataBtn.click({ force: true }) - cy.findByText('New Dataset').should('be.visible').click({ force: true }) - }) - - cy.visit('/spa/collections') - - cy.get('main').within(() => { - const addDataBtn = cy.findByRole('button', { name: /Add Data/i }) - addDataBtn.should('exist') - addDataBtn.click({ force: true }) - cy.findByText('New Dataset').should('be.visible').click({ force: true }) - }) - cy.get(`h1`) - .findByText(/Create Dataset/i) - .should('exist') - }) describe.skip('Currently skipping all tests as we are only rendering an infinite scrollable container. Please refactor these tests if a toggle button is added to switch between pagination and infinite scroll.', () => { it('navigates to the correct page of the datasets list when passing the page query param', () => { diff --git a/tests/e2e-integration/e2e/sections/collection/CollectionItemsPanel.spec.ts b/tests/e2e-integration/e2e/sections/collection/CollectionItemsPanel.spec.ts index 37e33b7e2..0948c287f 100644 --- a/tests/e2e-integration/e2e/sections/collection/CollectionItemsPanel.spec.ts +++ b/tests/e2e-integration/e2e/sections/collection/CollectionItemsPanel.spec.ts @@ -33,18 +33,26 @@ function extractInfoFromInterceptedResponse(interception: Interception) { } describe('Collection Items Panel', () => { - before(() => { - TestsUtils.setup() - TestsUtils.login() - }) - - beforeEach(async () => { - cy.intercept(SEARCH_ENDPOINT_REGEX).as('getCollectionItems') - - // Creates 8 datasets with 1 file each - for (const _number of numbersOfDatasetsToCreate) { - await DatasetHelper.createWithFile(FileHelper.create()) - } + // before(() => { + // TestsUtils.setup() + // TestsUtils.login() + // }) + + beforeEach(() => { + TestsUtils.login().then((token) => { + if (!token) { + throw new Error('Token not found after Keycloak login') + } + + cy.wrap(TestsUtils.setup(token)).then(async () => { + cy.intercept(SEARCH_ENDPOINT_REGEX).as('getCollectionItems') + + // Creates 8 datasets with 1 file each + for (const _number of numbersOfDatasetsToCreate) { + await DatasetHelper.createWithFile(FileHelper.create()) + } + }) + }) }) afterEach(() => { diff --git a/tests/e2e-integration/e2e/sections/create-collection/CreateCollection.spec.tsx b/tests/e2e-integration/e2e/sections/create-collection/CreateCollection.spec.tsx index c0a22da7f..8673b2602 100644 --- a/tests/e2e-integration/e2e/sections/create-collection/CreateCollection.spec.tsx +++ b/tests/e2e-integration/e2e/sections/create-collection/CreateCollection.spec.tsx @@ -5,8 +5,13 @@ const CREATE_COLLECTION_PAGE_URL = '/spa/collections/root/create' describe('Create Collection', () => { beforeEach(() => { - TestsUtils.login() - TestsUtils.setup() + TestsUtils.login().then((token) => { + if (!token) { + throw new Error('Token not found after Keycloak login') + } + + cy.wrap(TestsUtils.setup(token)) + }) }) it('visits the Create Collection Page as a logged in user', () => { diff --git a/tests/e2e-integration/e2e/sections/create-dataset/CreateDataset.spec.tsx b/tests/e2e-integration/e2e/sections/create-dataset/CreateDataset.spec.tsx index a31b2c112..7791c9010 100644 --- a/tests/e2e-integration/e2e/sections/create-dataset/CreateDataset.spec.tsx +++ b/tests/e2e-integration/e2e/sections/create-dataset/CreateDataset.spec.tsx @@ -5,8 +5,13 @@ const CREATE_DATASET_PAGE_URL = '/spa/datasets/root/create' describe('Create Dataset', () => { beforeEach(() => { - TestsUtils.login() - TestsUtils.setup() + TestsUtils.login().then((token) => { + if (!token) { + throw new Error('Token not found after Keycloak login') + } + + cy.wrap(TestsUtils.setup(token)) + }) }) it('visits the Create Dataset Page as a logged in user', () => { diff --git a/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx b/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx index d78bcae85..daadb41bc 100644 --- a/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx +++ b/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx @@ -14,13 +14,17 @@ type Dataset = { } const DRAFT_PARAM = DatasetNonNumericVersionSearchParam.DRAFT -describe('Dataset', () => { - before(() => { - TestsUtils.setup() - }) +// TODO:ME - User not admin cant publish dataset, maybe superuser lookup and give permission to test user? +describe('Dataset', () => { beforeEach(() => { - TestsUtils.login() + TestsUtils.login().then((token) => { + if (!token) { + throw new Error('Token not found after Keycloak login') + } + + cy.wrap(TestsUtils.setup(token)) + }) }) describe('Visit the Dataset Page as a logged in user', () => { diff --git a/tests/e2e-integration/e2e/sections/edit-dataset-metadata/EditDatasetMetadata.spec.tsx b/tests/e2e-integration/e2e/sections/edit-dataset-metadata/EditDatasetMetadata.spec.tsx index 03f60ae34..39a40cdec 100644 --- a/tests/e2e-integration/e2e/sections/edit-dataset-metadata/EditDatasetMetadata.spec.tsx +++ b/tests/e2e-integration/e2e/sections/edit-dataset-metadata/EditDatasetMetadata.spec.tsx @@ -8,12 +8,14 @@ import { DatasetHelper } from '../../../shared/datasets/DatasetHelper' import { QueryParamKey, Route } from '../../../../../src/sections/Route.enum' describe('Edit Dataset metadata', () => { - before(() => { - TestsUtils.setup() - }) - beforeEach(() => { - TestsUtils.login() + TestsUtils.login().then((token) => { + if (!token) { + throw new Error('Token not found after Keycloak login') + } + + cy.wrap(TestsUtils.setup(token)) + }) }) it('visits the Edit Dataset Metadata Page as a logged in user', () => { @@ -65,9 +67,26 @@ describe('Edit Dataset metadata', () => { }) }) - it('redirects to the Log In page when the user is not authenticated', () => { - cy.wrap(TestsUtils.logout()) + it('should redirect the user to the Login page when the user is not authenticated', () => { + const datasetTitle = faker.lorem.sentence() + + cy.wrap(DatasetHelper.createWithTitle(datasetTitle), { timeout: 10000 }).then((dataset) => { + const searchParams = new URLSearchParams() + searchParams.set(QueryParamKey.PERSISTENT_ID, dataset.persistentId) + searchParams.set(QueryParamKey.VERSION, DatasetNonNumericVersionSearchParam.DRAFT) + + const editDatasetMetadataUrl = `/spa${Route.EDIT_DATASET_METADATA}?${searchParams.toString()}` + + TestsUtils.logout() + + cy.visit(editDatasetMetadataUrl) + + // Check if the Keycloak login form is present + cy.get('#kc-form-login').should('exist') + }) + }) + it('should redirect the user back to the edit dataset metadata page after a successful login', () => { const datasetTitle = faker.lorem.sentence() cy.wrap(DatasetHelper.createWithTitle(datasetTitle), { timeout: 10000 }).then((dataset) => { @@ -77,10 +96,25 @@ describe('Edit Dataset metadata', () => { const editDatasetMetadataUrl = `/spa${Route.EDIT_DATASET_METADATA}?${searchParams.toString()}` + TestsUtils.logout() + cy.visit(editDatasetMetadataUrl) - cy.get('#login-container').should('exist') - cy.url().should('include', '/loginpage.xhtml') + // Check if the Keycloak login form is present + cy.get('#kc-form-login').should('exist') + + // Enter the credentials in the Keycloak login form + TestsUtils.enterCredentialsInKeycloak() + + // Check if the user is redirected back to the edit dataset metadata page + cy.url().should('eq', `${Cypress.config().baseUrl as string}${editDatasetMetadataUrl}`) + + cy.findByRole('link', { name: 'Root' }) + .closest('.breadcrumb') + .within(() => { + cy.findByRole('link', { name: datasetTitle }).should('exist') + cy.findByText('Edit Dataset Metadata').should('exist') + }) }) }) }) diff --git a/tests/e2e-integration/e2e/sections/file/File.spec.tsx b/tests/e2e-integration/e2e/sections/file/File.spec.tsx index c19a27e4b..3e1ee6e3b 100644 --- a/tests/e2e-integration/e2e/sections/file/File.spec.tsx +++ b/tests/e2e-integration/e2e/sections/file/File.spec.tsx @@ -3,14 +3,19 @@ import { DatasetHelper } from '../../../shared/datasets/DatasetHelper' import { DatasetLabelValue } from '../../../../../src/dataset/domain/models/Dataset' import { FileHelper } from '../../../shared/files/FileHelper' -describe('File', () => { - before(() => { - TestsUtils.setup() - }) +// TODO:ME - User not admin cant publish dataset, maybe superuser lookup and give permission to test user? +describe('File', () => { beforeEach(() => { - TestsUtils.login() + TestsUtils.login().then((token) => { + if (!token) { + throw new Error('Token not found after Keycloak login') + } + + cy.wrap(TestsUtils.setup(token)) + }) }) + describe('Visit the File Page as a logged in user', () => { it('successfully loads a file in draft mode', () => { cy.wrap( @@ -32,7 +37,7 @@ describe('File', () => { }) }) - it('successfully loads a published file when the user is not authenticated', () => { + it.only('successfully loads a published file when the user is not authenticated', () => { cy.wrap( DatasetHelper.createWithFileAndPublish(FileHelper.create()).then( (datasetResponse) => datasetResponse.file @@ -57,6 +62,8 @@ describe('File', () => { }) it('loads page not found when the user is not authenticated and tries to access a draft', () => { + TestsUtils.logout() + cy.wrap( DatasetHelper.createWithFile(FileHelper.create()).then( (datasetResponse) => datasetResponse.file From 12fe3f7993297f3ce740656a5c3eda767dcc15e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 5 Nov 2024 13:20:21 -0300 Subject: [PATCH 29/97] test: set regular user as superuser in setup --- .../e2e/sections/dataset/Dataset.spec.tsx | 35 +++++++------------ .../shared/DataverseApiHelper.ts | 34 ++++++++++++++++-- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx b/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx index daadb41bc..12941cc97 100644 --- a/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx +++ b/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx @@ -8,6 +8,7 @@ import { FileHelper } from '../../../shared/files/FileHelper' import moment from 'moment-timezone' import { CollectionHelper } from '../../../shared/collection/CollectionHelper' import { FILES_TAB_INFINITE_SCROLL_ENABLED } from '../../../../../src/sections/dataset/config' +import { DateHelper } from '@/shared/helpers/DateHelper' type Dataset = { datasetVersion: { metadataBlocks: { citation: { fields: { value: string }[] } } } @@ -33,17 +34,14 @@ describe('Dataset', () => { .its('persistentId') .then((persistentId: string) => { cy.visit(`/spa/datasets?persistentId=${persistentId}&version=${DRAFT_PARAM}`) - cy.fixture('dataset-finch1.json').then((dataset: Dataset) => { cy.findByRole('heading', { name: dataset.datasetVersion.metadataBlocks.citation.fields[0].value }).should('exist') cy.findByText(DatasetLabelValue.DRAFT).should('exist') cy.findByText(DatasetLabelValue.UNPUBLISHED).should('exist') - cy.findByText('Metadata').should('exist') cy.findByText('Files').should('exist') - cy.findByRole('button', { name: 'Edit Dataset' }).should('exist').click() cy.findByRole('button', { name: 'Permissions' }).should('exist').click() cy.findByRole('button', { name: 'Dataset' }).should('exist') @@ -163,7 +161,7 @@ describe('Dataset', () => { cy.wrap(DatasetHelper.create().then((dataset) => DatasetHelper.publish(dataset.persistentId))) .its('persistentId') .then((persistentId: string) => { - cy.wrap(TestsUtils.logout()) + TestsUtils.logout() cy.wait(1500) // Wait for the dataset to be published cy.visit(`/spa/datasets?persistentId=${persistentId}`) @@ -185,7 +183,7 @@ describe('Dataset', () => { cy.wrap(DatasetHelper.create()) .its('persistentId') .then((persistentId: string) => { - cy.wrap(TestsUtils.logout()) + TestsUtils.logout() cy.visit(`/spa/datasets?persistentId=${persistentId}&version=${DRAFT_PARAM}`) cy.findByText('Page Not Found').should('exist') @@ -416,7 +414,7 @@ describe('Dataset', () => { .its('persistentId') .then((persistentId: string) => { cy.wait(1500) // Wait for the dataset to be published - cy.wrap(TestsUtils.logout()) + TestsUtils.logout() cy.visit(`/spa/datasets?persistentId=${persistentId}`) @@ -462,7 +460,7 @@ describe('Dataset', () => { .then((persistentId: string) => { cy.wait(1500) // Wait for the dataset to be published - cy.wrap(TestsUtils.logout()) + TestsUtils.logout() cy.visit(`/spa/datasets?persistentId=${persistentId}`) @@ -477,15 +475,13 @@ describe('Dataset', () => { cy.findByRole('button', { name: 'Access File' }).as('accessButton') cy.get('@accessButton').should('exist') cy.get('@accessButton').click() - cy.findByText('Restricted').should('exist') + // cy.findByText(new RegExp('^Restricted$', 'i')).should('exist') + cy.findByText('Restricted', { exact: true }).should('exist') }) }) it('loads the embargoed files', () => { - cy.window().then((win) => { - // Get the browser's locale from the window object - const browserLocale = win.navigator.language - + cy.window().then(() => { // Create a moment object in UTC and set the time to 12 AM (midnight) const utcDate = moment.utc().startOf('day') @@ -493,15 +489,7 @@ describe('Dataset', () => { utcDate.add(100, 'years') const dateString = utcDate.format('YYYY-MM-DD') - // Use the browser's locale to format the date using Intl.DateTimeFormat - const options: Intl.DateTimeFormatOptions = { - year: 'numeric', - month: 'short', - day: 'numeric' - } - const expectedDate = new Intl.DateTimeFormat(browserLocale, options).format( - utcDate.toDate() - ) + const expectedDate = DateHelper.toDisplayFormat(utcDate.toDate()) cy.wrap( DatasetHelper.createWithFiles(FileHelper.createMany(1)).then((dataset) => @@ -525,6 +513,8 @@ describe('Dataset', () => { cy.findByText(/Deposited/).should('exist') cy.findByText(`Draft: will be embargoed until ${expectedDate}`).should('exist') + // Draft: will be embargoed until Nov 4, 2124 + cy.get('#edit-files-menu').should('exist') cy.findByRole('button', { name: 'Access File' }).as('accessButton') @@ -690,7 +680,8 @@ describe('Dataset', () => { }) }) - it('shows the thumbnail for a file', () => { + // TODO:ME - http://localhost:8000/api/access/datafile/229?imageThumb=400 returns 403 Forbidden + it.skip('shows the thumbnail for a file', () => { cy.wrap(FileHelper.createImage().then((file) => DatasetHelper.createWithFiles([file]))) .its('persistentId') .then((persistentId: string) => { diff --git a/tests/e2e-integration/shared/DataverseApiHelper.ts b/tests/e2e-integration/shared/DataverseApiHelper.ts index f3918a4c2..53d4e49a4 100644 --- a/tests/e2e-integration/shared/DataverseApiHelper.ts +++ b/tests/e2e-integration/shared/DataverseApiHelper.ts @@ -1,4 +1,4 @@ -import axios, { AxiosRequestConfig } from 'axios' +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' import { TestsUtils } from './TestsUtils' export class DataverseApiHelper { @@ -14,7 +14,9 @@ export class DataverseApiHelper { this.API_URL = `${TestsUtils.DATAVERSE_BACKEND_URL}/api/v1` try { - const createdApiToken = await this.createAndGetApiKeyWithBearerToken(bearerToken) + await this.setLoggedInUserAsSuperUser() + + const createdApiToken = await this.createAndGetApiTokenWithBearerToken(bearerToken) this.API_TOKEN = createdApiToken @@ -94,7 +96,7 @@ export class DataverseApiHelper { return formData } - static async createAndGetApiKeyWithBearerToken(bearerToken: string): Promise { + static async createAndGetApiTokenWithBearerToken(bearerToken: string): Promise { console.log( '%cCreating test API key...', 'background: blue; color: white; padding: 2px; border-radius: 4px;' @@ -117,4 +119,30 @@ export class DataverseApiHelper { return apiKey } + + static async setLoggedInUserAsSuperUser(): Promise { + const API_ALLOW_TOKEN_LOOKUP_ENDPOINT = '/admin/settings/:AllowApiTokenLookupViaApi' + const API_KEY_USER_ENDPOINT = '/builtin-users/dataverseAdmin/api-token' + const API_KEY_USER_PASSWORD = 'admin1' + + // Get API key from superuser dataverseAdmin + await axios.put(`${this.API_URL}${API_ALLOW_TOKEN_LOOKUP_ENDPOINT}`, 'true') + + // Get API key from superuser dataverseAdmin + const { + data: { + data: { message: superuserApiToken } + } + }: AxiosResponse<{ data: { message: string } }> = await axios.get( + `${this.API_URL}${API_KEY_USER_ENDPOINT}?password=${API_KEY_USER_PASSWORD}` + ) + + // Set superuser status for the user authenticated via OIDC + await axios.put(`${this.API_URL}/admin/superuser/admin`, 'true', { + headers: { + 'X-Dataverse-key': superuserApiToken, + 'Content-Type': 'application/json' + } + }) + } } From da82d11e54a84edc64f7e61bcd523b07ded982b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 5 Nov 2024 15:31:57 -0300 Subject: [PATCH 30/97] feat: change link to old login for button link --- .../items-list/NoItemsMessage.tsx | 19 +++++++++++++-- .../access-file-menu/RequestAccessModal.tsx | 23 ++++++++++++++----- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/sections/collection/collection-items-panel/items-list/NoItemsMessage.tsx b/src/sections/collection/collection-items-panel/items-list/NoItemsMessage.tsx index 6b568040f..cf33b4cf0 100644 --- a/src/sections/collection/collection-items-panel/items-list/NoItemsMessage.tsx +++ b/src/sections/collection/collection-items-panel/items-list/NoItemsMessage.tsx @@ -1,7 +1,11 @@ +import { useContext } from 'react' import { Trans, useTranslation } from 'react-i18next' +import { AuthContext } from 'react-oauth2-code-pkce' +import { useLocation } from 'react-router-dom' import { useSession } from '@/sections/session/SessionContext' -import { Route } from '@/sections/Route.enum' +import { Button } from '@iqss/dataverse-design-system' import { CollectionItemType } from '@/collection/domain/models/CollectionItemType' +import { encodeReturnToPathInStateQueryParam } from '@/sections/auth-callback/AuthCallback' import styles from './ItemsList.module.scss' interface NoItemsMessageProps { @@ -11,6 +15,8 @@ interface NoItemsMessageProps { export function NoItemsMessage({ itemsTypesSelected }: NoItemsMessageProps) { const { t } = useTranslation('collection') const { user } = useSession() + const { logIn: oidcLogin } = useContext(AuthContext) + const { pathname, search } = useLocation() const itemTypeMessages = { all: t('noItemsMessage.itemTypeMessage.all'), @@ -65,7 +71,16 @@ export function NoItemsMessage({ itemsTypesSelected }: NoItemsMessageProps) { i18nKey="noItemsMessage.anonymous" values={{ typeOfEmptyItems: messageKey }} components={{ - 1: log in + 1: ( + + ) }} /> )} diff --git a/src/sections/file/file-action-buttons/access-file-menu/RequestAccessModal.tsx b/src/sections/file/file-action-buttons/access-file-menu/RequestAccessModal.tsx index a0c4fd7ae..8deb588b5 100644 --- a/src/sections/file/file-action-buttons/access-file-menu/RequestAccessModal.tsx +++ b/src/sections/file/file-action-buttons/access-file-menu/RequestAccessModal.tsx @@ -1,11 +1,13 @@ -import { Button, Col, DropdownButtonItem, Modal } from '@iqss/dataverse-design-system' -import { useSession } from '../../../session/SessionContext' -import { FormEvent, useState } from 'react' -import { Form } from '@iqss/dataverse-design-system' +import { FormEvent, useContext, useState } from 'react' +import { AuthContext } from 'react-oauth2-code-pkce' +import { useLocation } from 'react-router-dom' +import { Button, Col, DropdownButtonItem, Modal, Form } from '@iqss/dataverse-design-system' import { ExclamationTriangle } from 'react-bootstrap-icons' +import { useTranslation } from 'react-i18next' +import { useSession } from '../../../session/SessionContext' import { Route } from '../../../Route.enum' +import { encodeReturnToPathInStateQueryParam } from '@/sections/auth-callback/AuthCallback' import styles from './AccessFileMenu.module.scss' -import { useTranslation } from 'react-i18next' interface RequestAccessButtonProps { fileId: number @@ -80,12 +82,21 @@ const RequestAccessForm = ({ const RequestAccessLoginMessage = ({ handleClose }: { handleClose: () => void }) => { const { t } = useTranslation('files') + const { logIn: oidcLogin } = useContext(AuthContext) + const { pathname, search } = useLocation() + return ( <>

You need to Sign Up or{' '} - Log In to request access. + {' '} + to request access.

From 7975ce9152d5ba863f5ce013dc7268bbdcb383cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 7 Nov 2024 08:29:22 -0300 Subject: [PATCH 31/97] tests: more work on setup etc --- src/shared/helpers/Utils.ts | 10 ++++++++++ .../e2e/sections/collection/Collection.spec.ts | 10 +++++----- .../collection/CollectionItemsPanel.spec.ts | 5 ----- .../e2e/sections/dataset/Dataset.spec.tsx | 5 +---- .../e2e/sections/file/File.spec.tsx | 10 +++------- .../shared/DataverseApiHelper.ts | 5 +++-- tests/e2e-integration/shared/TestsUtils.ts | 17 +++++------------ tests/support/commands.tsx | 3 ++- tests/support/e2e.ts | 11 ++++++----- 9 files changed, 35 insertions(+), 41 deletions(-) diff --git a/src/shared/helpers/Utils.ts b/src/shared/helpers/Utils.ts index ba75bdaae..d819fa1cb 100644 --- a/src/shared/helpers/Utils.ts +++ b/src/shared/helpers/Utils.ts @@ -12,4 +12,14 @@ export class Utils { timeoutId = setTimeout(() => fn(...args), delay) } } + + static getLocalStorageItem(key: string): T | null { + try { + const item = localStorage.getItem(key) + return item ? (JSON.parse(item) as T) : null + } catch (error) { + console.error(`Error parsing localStorage key "${key}":`, error) + return null + } + } } diff --git a/tests/e2e-integration/e2e/sections/collection/Collection.spec.ts b/tests/e2e-integration/e2e/sections/collection/Collection.spec.ts index b93918f0d..ab7599de3 100644 --- a/tests/e2e-integration/e2e/sections/collection/Collection.spec.ts +++ b/tests/e2e-integration/e2e/sections/collection/Collection.spec.ts @@ -26,7 +26,7 @@ describe('Collection Page', () => { cy.wait(1_000) cy.visit('/spa/collections') - cy.findByText(/Dataverse Admin/i).should('exist') + cy.findByText(/Dataverse User/i).should('exist') cy.findByText(title).should('be.visible') cy.findByText(title).click({ force: true }) @@ -59,7 +59,7 @@ describe('Collection Page', () => { cy.wrap(DatasetHelper.createMany(12), { timeout: 10000 }).then(() => { cy.visit('/spa/collections?page=2') cy.findAllByText(/Root/i).should('exist') - cy.findByText(/Dataverse Admin/i).should('exist') + cy.findByText(/Dataverse User/i).should('exist') cy.findByText('11 to 12 of 12 Datasets').should('exist') }) @@ -70,7 +70,7 @@ describe('Collection Page', () => { cy.visit('/spa/collections') cy.findAllByText(/Root/i).should('exist') - cy.findByText(/Dataverse Admin/i).should('exist') + cy.findByText(/Dataverse User/i).should('exist') cy.findByRole('button', { name: 'Next' }).click() cy.findByText('11 to 12 of 12 Datasets').should('exist') @@ -83,7 +83,7 @@ describe('Collection Page', () => { cy.visit('/spa/collections?page=2') cy.findAllByText(/Root/i).should('exist') - cy.findByText(/Dataverse Admin/i).should('exist') + cy.findByText(/Dataverse User/i).should('exist') cy.findByText('11 to 12 of 12 Datasets').should('exist') cy.findByRole('button', { name: '1' }).click({ force: true }) @@ -103,7 +103,7 @@ describe('Collection Page', () => { cy.visit('/spa/collections/collection-1') cy.findAllByText(/Scientific Research/i).should('exist') - cy.findByText(/Dataverse Admin/i).should('exist') + cy.findByText(/Dataverse User/i).should('exist') }) }) }) diff --git a/tests/e2e-integration/e2e/sections/collection/CollectionItemsPanel.spec.ts b/tests/e2e-integration/e2e/sections/collection/CollectionItemsPanel.spec.ts index 0948c287f..733d17ac9 100644 --- a/tests/e2e-integration/e2e/sections/collection/CollectionItemsPanel.spec.ts +++ b/tests/e2e-integration/e2e/sections/collection/CollectionItemsPanel.spec.ts @@ -33,11 +33,6 @@ function extractInfoFromInterceptedResponse(interception: Interception) { } describe('Collection Items Panel', () => { - // before(() => { - // TestsUtils.setup() - // TestsUtils.login() - // }) - beforeEach(() => { TestsUtils.login().then((token) => { if (!token) { diff --git a/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx b/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx index 12941cc97..4047ba2ca 100644 --- a/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx +++ b/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx @@ -15,8 +15,6 @@ type Dataset = { } const DRAFT_PARAM = DatasetNonNumericVersionSearchParam.DRAFT -// TODO:ME - User not admin cant publish dataset, maybe superuser lookup and give permission to test user? - describe('Dataset', () => { beforeEach(() => { TestsUtils.login().then((token) => { @@ -680,8 +678,7 @@ describe('Dataset', () => { }) }) - // TODO:ME - http://localhost:8000/api/access/datafile/229?imageThumb=400 returns 403 Forbidden - it.skip('shows the thumbnail for a file', () => { + it('shows the thumbnail for a file', () => { cy.wrap(FileHelper.createImage().then((file) => DatasetHelper.createWithFiles([file]))) .its('persistentId') .then((persistentId: string) => { diff --git a/tests/e2e-integration/e2e/sections/file/File.spec.tsx b/tests/e2e-integration/e2e/sections/file/File.spec.tsx index 3e1ee6e3b..caddb0477 100644 --- a/tests/e2e-integration/e2e/sections/file/File.spec.tsx +++ b/tests/e2e-integration/e2e/sections/file/File.spec.tsx @@ -3,8 +3,6 @@ import { DatasetHelper } from '../../../shared/datasets/DatasetHelper' import { DatasetLabelValue } from '../../../../../src/dataset/domain/models/Dataset' import { FileHelper } from '../../../shared/files/FileHelper' -// TODO:ME - User not admin cant publish dataset, maybe superuser lookup and give permission to test user? - describe('File', () => { beforeEach(() => { TestsUtils.login().then((token) => { @@ -37,7 +35,7 @@ describe('File', () => { }) }) - it.only('successfully loads a published file when the user is not authenticated', () => { + it('successfully loads a published file when the user is not authenticated', () => { cy.wrap( DatasetHelper.createWithFileAndPublish(FileHelper.create()).then( (datasetResponse) => datasetResponse.file @@ -46,7 +44,7 @@ describe('File', () => { ) .its('id') .then((id: string) => { - cy.wrap(TestsUtils.logout()) + TestsUtils.logout() cy.visit(`/spa/files?id=${id}`) cy.findByRole('heading', { name: 'blob' }).should('exist') @@ -62,8 +60,6 @@ describe('File', () => { }) it('loads page not found when the user is not authenticated and tries to access a draft', () => { - TestsUtils.logout() - cy.wrap( DatasetHelper.createWithFile(FileHelper.create()).then( (datasetResponse) => datasetResponse.file @@ -71,7 +67,7 @@ describe('File', () => { ) .its('id') .then((id: string) => { - cy.wrap(TestsUtils.logout()) + TestsUtils.logout() cy.visit(`/spa/files?id=${id}`) cy.findByText('Page Not Found').should('exist') diff --git a/tests/e2e-integration/shared/DataverseApiHelper.ts b/tests/e2e-integration/shared/DataverseApiHelper.ts index 53d4e49a4..08bd7c370 100644 --- a/tests/e2e-integration/shared/DataverseApiHelper.ts +++ b/tests/e2e-integration/shared/DataverseApiHelper.ts @@ -53,7 +53,8 @@ export class DataverseApiHelper { ): Promise { console.log( '%cMaking request...', - 'background: violet; color: white; padding: 2px; border-radius: 4px;' + 'background: violet; color: white; padding: 2px; border-radius: 4px;', + url ) const isFormData = contentType === 'multipart/form-data' @@ -138,7 +139,7 @@ export class DataverseApiHelper { ) // Set superuser status for the user authenticated via OIDC - await axios.put(`${this.API_URL}/admin/superuser/admin`, 'true', { + await axios.put(`${this.API_URL}/admin/superuser/${TestsUtils.USER_USERNAME}`, 'true', { headers: { 'X-Dataverse-key': superuserApiToken, 'Content-Type': 'application/json' diff --git a/tests/e2e-integration/shared/TestsUtils.ts b/tests/e2e-integration/shared/TestsUtils.ts index 4e6585838..9160504b7 100644 --- a/tests/e2e-integration/shared/TestsUtils.ts +++ b/tests/e2e-integration/shared/TestsUtils.ts @@ -6,6 +6,9 @@ import { DATAVERSE_BACKEND_URL } from '../../../src/config' export class TestsUtils { static readonly DATAVERSE_BACKEND_URL = DATAVERSE_BACKEND_URL + static readonly USER_EMAIL = 'dataverse-user@mailinator.com' + static readonly USER_PASSWORD = 'user' + static readonly USER_USERNAME = 'user' static async setup(bearerToken: string) { ApiConfig.init(`${this.DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.API_KEY) @@ -56,18 +59,8 @@ export class TestsUtils { } static enterCredentialsInKeycloak() { - cy.get('#username').type('dataverse-admin@mailinator.com') - cy.get('#password').type('admin') + cy.get('#username').type(this.USER_EMAIL) + cy.get('#password').type(this.USER_PASSWORD) cy.get('#kc-login').click() } - - static getLocalStorageItem(key: string): T | null { - try { - const item = localStorage.getItem(key) - return item ? (JSON.parse(item) as T) : null - } catch (error) { - console.error(`Error parsing localStorage key "${key}":`, error) - return null - } - } } diff --git a/tests/support/commands.tsx b/tests/support/commands.tsx index 5f337e126..3f76c8c15 100644 --- a/tests/support/commands.tsx +++ b/tests/support/commands.tsx @@ -47,6 +47,7 @@ import { UserRepository } from '../../src/users/domain/repositories/UserReposito import { SessionProvider } from '../../src/sections/session/SessionProvider' import { MemoryRouter } from 'react-router-dom' import { TestsUtils } from '@tests/e2e-integration/shared/TestsUtils' +import { Utils } from '@/shared/helpers/Utils' // Define your custom mount function @@ -87,7 +88,7 @@ Cypress.Commands.add('login', () => { cy.url() .should('eq', `${Cypress.config().baseUrl as string}/spa`) .then(() => { - const token = TestsUtils.getLocalStorageItem('ROCP_token') + const token = Utils.getLocalStorageItem('ROCP_token') return cy.wrap(token) }) diff --git a/tests/support/e2e.ts b/tests/support/e2e.ts index cda0896ec..50536e762 100644 --- a/tests/support/e2e.ts +++ b/tests/support/e2e.ts @@ -15,13 +15,14 @@ // Import commands.js using ES2015 syntax: import '../../tests/support/commands' -import { ApiConfig } from '@iqss/dataverse-client-javascript/dist/core' -import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' -import { DATAVERSE_BACKEND_URL } from '../../src/config' +// import { ApiConfig } from '@iqss/dataverse-client-javascript/dist/core' +// import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' +// import { DATAVERSE_BACKEND_URL } from '../../src/config' -ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.SESSION_COOKIE) +// TODO:ME Why do we need api config in here? +// ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.SESSION_COOKIE) -//https://github.com/cypress-io/cypress/issues/18182 +// This global declaration is to get automatic typescript inferring for wrap https://github.com/cypress-io/cypress/issues/18182 declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { From e5fa8ee4ab514f6d0d37daf68128f6f8f4e8fb71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 7 Nov 2024 08:29:38 -0300 Subject: [PATCH 32/97] feat: add axios instance --- src/axiosInstance.ts | 23 +++++++++++++++++++ .../FileJSDataverseRepository.ts | 19 ++++++++------- 2 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 src/axiosInstance.ts diff --git a/src/axiosInstance.ts b/src/axiosInstance.ts new file mode 100644 index 000000000..c57d6e38f --- /dev/null +++ b/src/axiosInstance.ts @@ -0,0 +1,23 @@ +import axios from 'axios' +import { DATAVERSE_BACKEND_URL } from './config' +import { Utils } from './shared/helpers/Utils' + +/** + * This instance is used to make requests that we do not do through js-dataverse + */ + +const axiosInstance = axios.create({ + baseURL: DATAVERSE_BACKEND_URL, + withCredentials: false +}) + +axiosInstance.interceptors.request.use((config) => { + const token = Utils.getLocalStorageItem('ROCP_token') + + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config +}) + +export { axiosInstance } diff --git a/src/files/infrastructure/FileJSDataverseRepository.ts b/src/files/infrastructure/FileJSDataverseRepository.ts index 27552ddb9..15fb2eaad 100644 --- a/src/files/infrastructure/FileJSDataverseRepository.ts +++ b/src/files/infrastructure/FileJSDataverseRepository.ts @@ -1,3 +1,5 @@ +import { AxiosResponse } from 'axios' +import { axiosInstance } from '@/axiosInstance' import { FileRepository } from '../domain/repositories/FileRepository' import { FileDownloadMode, FileTabularData } from '../domain/models/FileMetadata' import { FilesCountInfo } from '../domain/models/FilesCountInfo' @@ -172,15 +174,16 @@ export class FileJSDataverseRepository implements FileRepository { } private static getThumbnailById(id: number): Promise { - return fetch(`${this.DATAVERSE_BACKEND_URL}/api/access/datafile/${id}?imageThumb=400`) - .then((response) => { - if (!response.ok) { - throw new Error('Network response was not ok') - } - return response.blob() + return axiosInstance + .get(`${this.DATAVERSE_BACKEND_URL}/api/access/datafile/${id}?imageThumb=400`, { + responseType: 'blob' }) - .then((blob) => { - return URL.createObjectURL(blob) + .then((res: AxiosResponse) => { + const blob = res.data + + const objectURL = URL.createObjectURL(blob) + + return objectURL }) .catch(() => { return undefined From 885fb084ada71cb43b00cdde96a562fd8347c2da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 7 Nov 2024 08:42:07 -0300 Subject: [PATCH 33/97] feat: integration tests --- .../CollectionJSDataverseRepository.spec.ts | 27 +++++- .../DatasetJSDataverseRepository.spec.ts | 91 +++++++++++++++++-- 2 files changed, 108 insertions(+), 10 deletions(-) diff --git a/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts index 67515343c..421d92d6e 100644 --- a/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts @@ -7,6 +7,11 @@ import { UpwardHierarchyNode } from '../../../../src/shared/hierarchy/domain/models/UpwardHierarchyNode' import { Collection } from '../../../../src/collection/domain/models/Collection' +import { DATAVERSE_BACKEND_URL } from '@/config' +import { + ApiConfig, + DataverseApiAuthMechanism +} from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' const collectionRepository = new CollectionJSDataverseRepository() const collectionExpected: Collection = { @@ -27,14 +32,24 @@ const collectionExpected: Collection = { inputLevels: undefined } describe('Collection JSDataverse Repository', () => { - before(() => TestsUtils.setup()) beforeEach(() => { - TestsUtils.login() + TestsUtils.login().then((token) => { + if (!token) { + throw new Error('Token not found after Keycloak login') + } + + cy.wrap(TestsUtils.setup(token)) + }) }) it('gets the collection by id', async () => { const collectionResponse = await CollectionHelper.create('new-collection') - console.log('collectionResponse', collectionResponse.id) + + // Change the api config to use bearer token + cy.wrap( + ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ) + await collectionRepository.getById(collectionResponse.id).then((collection) => { if (!collection) { throw new Error('Collection not found') @@ -47,6 +62,12 @@ describe('Collection JSDataverse Repository', () => { const timestamp = new Date().valueOf() const uniqueCollectionId = `test-publish-collection-${timestamp}` const collectionResponse = await CollectionHelper.create(uniqueCollectionId) + + // Change the api config to use bearer token + cy.wrap( + ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ) + await collectionRepository.publish(collectionResponse.id) await collectionRepository.getById(collectionResponse.id).then((collection) => { if (!collection) { diff --git a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts index 247aa1fc3..4e8810717 100644 --- a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts @@ -22,6 +22,9 @@ import { DatasetDTO } from '../../../../src/dataset/domain/useCases/DTOs/Dataset import { CollectionHelper } from '../../shared/collection/CollectionHelper' const DRAFT_PARAM = DatasetNonNumericVersion.DRAFT import { VersionUpdateType } from '../../../../src/dataset/domain/models/VersionUpdateType' +import { ApiConfig } from '@iqss/dataverse-client-javascript' +import { DATAVERSE_BACKEND_URL } from '@/config' +import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' chai.use(chaiAsPromised) const expect = chai.expect @@ -131,20 +134,38 @@ const datasetData = (persistentId: string, versionId: number) => { ] } } + +// TODO:ME Some tests are failing, for dataset permissions is not matching + const collectionId = 'DatasetJSDataverseRepository' const datasetRepository = new DatasetJSDataverseRepository() describe('Dataset JSDataverse Repository', () => { - before(() => { - TestsUtils.setup() - TestsUtils.login().then(() => CollectionHelper.createAndPublish(collectionId)) - }) + // before(() => { + // TestsUtils.setup() + // TestsUtils.login().then(() => CollectionHelper.createAndPublish(collectionId)) + // }) + // beforeEach(() => { + // TestsUtils.login() + // }) + beforeEach(() => { - TestsUtils.login() + TestsUtils.login().then((token) => { + if (!token) { + throw new Error('Token not found after Keycloak login') + } + + cy.wrap(TestsUtils.setup(token)).then(() => CollectionHelper.createAndPublish(collectionId)) + }) }) it('gets the dataset by persistentId', async () => { const datasetResponse = await DatasetHelper.create(collectionId) + // Change the api config to use bearer token + cy.wrap( + ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ) + await datasetRepository .getByPersistentId(datasetResponse.persistentId, DRAFT_PARAM) .then((dataset) => { @@ -166,17 +187,23 @@ describe('Dataset JSDataverse Repository', () => { }) }) - it('gets a published dataset by persistentId without user authentication', async () => { + it.only('gets a published dataset by persistentId without user authentication', async () => { const datasetResponse = await DatasetHelper.create(collectionId) await DatasetHelper.publish(datasetResponse.persistentId) await TestsUtils.wait(1500) - await TestsUtils.logout() + TestsUtils.logout() + + // Change the api config to use bearer token + cy.wrap( + ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ) await datasetRepository .getByPersistentId(datasetResponse.persistentId, '1.0') .then((dataset) => { + console.log(dataset) if (!dataset) { throw new Error('Dataset not found') } @@ -201,6 +228,8 @@ describe('Dataset JSDataverse Repository', () => { expectedPublicationDate ) expect(dataset.metadataBlocks[0].fields.citationDate).not.to.exist + + console.log(dataset.permissions) expect(dataset.permissions).to.deep.equal({ canDownloadFiles: true, canUpdateDataset: false, @@ -216,6 +245,12 @@ describe('Dataset JSDataverse Repository', () => { const datasetResponse = await DatasetHelper.create(collectionId) await DatasetHelper.publish(datasetResponse.persistentId) await TestsUtils.waitForNoLocks(datasetResponse.persistentId) + + // Change the api config to use bearer token + cy.wrap( + ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ) + await datasetRepository .getByPersistentId(datasetResponse.persistentId, '1.0') .then((dataset) => { @@ -251,6 +286,11 @@ describe('Dataset JSDataverse Repository', () => { it('gets the dataset by persistentId and version DRAFT keyword', async () => { const datasetResponse = await DatasetHelper.create(collectionId) + // Change the api config to use bearer token + cy.wrap( + ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ) + await datasetRepository .getByPersistentId(datasetResponse.persistentId, DRAFT_PARAM) .then((dataset) => { @@ -268,6 +308,11 @@ describe('Dataset JSDataverse Repository', () => { const datasetResponse = await DatasetHelper.create(collectionId) const privateUrlResponse = await DatasetHelper.createPrivateUrl(datasetResponse.id) + // Change the api config to use bearer token + cy.wrap( + ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ) + await datasetRepository.getByPrivateUrlToken(privateUrlResponse.token).then((dataset) => { if (!dataset) { throw new Error('Dataset not found') @@ -288,6 +333,11 @@ describe('Dataset JSDataverse Repository', () => { await DatasetHelper.setCitationDateFieldType(datasetResponse.persistentId, 'dateOfDeposit') + // Change the api config to use bearer token + cy.wrap( + ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ) + await datasetRepository .getByPersistentId(datasetResponse.persistentId, '1.0') .then((dataset) => { @@ -309,6 +359,11 @@ describe('Dataset JSDataverse Repository', () => { return DatasetHelper.createAndPublish(previewCollectionId).then((datasetResponse) => { const paginationInfo = new DatasetPaginationInfo(1, 20) + // Change the api config to use bearer token + cy.wrap( + ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ) + return datasetRepository .getAllWithCount(previewCollectionId, paginationInfo) .then((datasetsWithCount) => { @@ -330,6 +385,12 @@ describe('Dataset JSDataverse Repository', () => { await TestsUtils.wait(1500) await DatasetHelper.deaccession(datasetResponse.id) + + // Change the api config to use bearer token + cy.wrap( + ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ) + await datasetRepository.getByPersistentId(datasetResponse.persistentId).then((dataset) => { if (!dataset) { throw new Error('Dataset not found') @@ -343,6 +404,11 @@ describe('Dataset JSDataverse Repository', () => { const datasetResponse = await DatasetHelper.create(collectionId) await DatasetHelper.lock(datasetResponse.id, DatasetLockReason.FINALIZE_PUBLICATION) + // Change the api config to use bearer token + cy.wrap( + ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ) + await datasetRepository .getByPersistentId(datasetResponse.persistentId, DRAFT_PARAM) .then((dataset) => { @@ -362,6 +428,11 @@ describe('Dataset JSDataverse Repository', () => { }) it('creates a new dataset from DatasetDTO', async () => { + // Change the api config to use bearer token + cy.wrap( + ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ) + const datasetDTO: DatasetDTO = { metadataBlocks: [ { @@ -396,6 +467,12 @@ describe('Dataset JSDataverse Repository', () => { }) it('publishes a draft dataset', async () => { const datasetResponse = await DatasetHelper.create(collectionId) + + // Change the api config to use bearer token + cy.wrap( + ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ) + await datasetRepository.publish(datasetResponse.persistentId).then((response) => { expect(response).to.not.exist }) From f75fc383894bff77412f4c5d910f7de557f3db29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 25 Nov 2024 11:59:14 -0300 Subject: [PATCH 34/97] chore: update js-dataverse version --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index d57515ef7..2e5556840 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-pr201.c64af18", + "@iqss/dataverse-client-javascript": "2.0.0-pr224.045ca23", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -3675,9 +3675,9 @@ }, "node_modules/@iqss/dataverse-client-javascript": { "name": "@IQSS/dataverse-client-javascript", - "version": "2.0.0-pr201.c64af18", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr201.c64af18/7713e5dfc2f1f9c1a1b1095d03838744f21e2747", - "integrity": "sha512-muG2l/xQL62x5BndMae/b2+5EQQHa7n617VVazuQFCjDzZM2rJOf4dafLrmfd6+8hTq9CRfaqe9zV2h9H871XQ==", + "version": "2.0.0-pr224.045ca23", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr224.045ca23/50a1ad77bca4ca2a696085f56b79fecd2e72196b", + "integrity": "sha512-D0M5qaB9tpw9hFS9DzhLgLNW4wZj5+biHpUw1mmCfm2q93WL09mHZuHkB0dCnfGTVSj3TkVoSM3Xp8Cq/cdB+Q==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", diff --git a/package.json b/package.json index b7e62ea08..b76b1b83a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-pr201.c64af18", + "@iqss/dataverse-client-javascript": "2.0.0-pr224.045ca23", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", From d8a603b9d07323dacde26763e978e5446fc3b311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 25 Nov 2024 12:31:57 -0300 Subject: [PATCH 35/97] feat: use DV local storage key prefix and pass it trough js-dataverse --- src/App.tsx | 12 ++- src/axiosInstance.ts | 6 +- src/config.ts | 4 + .../CollectionJSDataverseRepository.spec.ts | 16 +++- .../DatasetJSDataverseRepository.spec.ts | 79 ++++++++++++++++--- tests/support/commands.tsx | 5 +- 6 files changed, 101 insertions(+), 21 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 82a418cc5..4e4c92be9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,13 +5,18 @@ import { Router } from './router' import { SessionProvider } from './sections/session/SessionProvider' import { UserJSDataverseRepository } from './users/infrastructure/repositories/UserJSDataverseRepository' import { Route } from './sections/Route.enum' -import { DATAVERSE_BACKEND_URL } from './config' +import { OIDC_AUTH_CONFIG, DATAVERSE_BACKEND_URL } from './config' import 'react-loading-skeleton/dist/skeleton.css' if (DATAVERSE_BACKEND_URL === '') { throw Error('VITE_DATAVERSE_BACKEND_URL environment variable should be specified.') } else { - ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) } const origin = window.location.origin @@ -26,7 +31,8 @@ const authConfig: TAuthConfig = { redirectUri: `${origin}${BASENAME_URL}${Route.AUTH_CALLBACK}`, scope: 'openid', autoLogin: false, - clearURL: false + clearURL: false, + storageKeyPrefix: OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX } const userRepository = new UserJSDataverseRepository() diff --git a/src/axiosInstance.ts b/src/axiosInstance.ts index c57d6e38f..927ce1e25 100644 --- a/src/axiosInstance.ts +++ b/src/axiosInstance.ts @@ -1,5 +1,5 @@ import axios from 'axios' -import { DATAVERSE_BACKEND_URL } from './config' +import { OIDC_AUTH_CONFIG, DATAVERSE_BACKEND_URL } from './config' import { Utils } from './shared/helpers/Utils' /** @@ -12,7 +12,9 @@ const axiosInstance = axios.create({ }) axiosInstance.interceptors.request.use((config) => { - const token = Utils.getLocalStorageItem('ROCP_token') + const token = Utils.getLocalStorageItem( + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}_token` + ) if (token) { config.headers.Authorization = `Bearer ${token}` diff --git a/src/config.ts b/src/config.ts index 4d4037d3e..8da214960 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1 +1,5 @@ export const DATAVERSE_BACKEND_URL = (import.meta.env.VITE_DATAVERSE_BACKEND_URL as string) ?? '' + +export const OIDC_AUTH_CONFIG = { + LOCAL_STORAGE_KEY_PREFIX: 'DV_' +} diff --git a/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts index 421d92d6e..3c534e37a 100644 --- a/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts @@ -7,7 +7,7 @@ import { UpwardHierarchyNode } from '../../../../src/shared/hierarchy/domain/models/UpwardHierarchyNode' import { Collection } from '../../../../src/collection/domain/models/Collection' -import { DATAVERSE_BACKEND_URL } from '@/config' +import { DATAVERSE_BACKEND_URL, OIDC_AUTH_CONFIG } from '@/config' import { ApiConfig, DataverseApiAuthMechanism @@ -47,7 +47,12 @@ describe('Collection JSDataverse Repository', () => { // Change the api config to use bearer token cy.wrap( - ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) ) await collectionRepository.getById(collectionResponse.id).then((collection) => { @@ -65,7 +70,12 @@ describe('Collection JSDataverse Repository', () => { // Change the api config to use bearer token cy.wrap( - ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) ) await collectionRepository.publish(collectionResponse.id) diff --git a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts index 4e8810717..dee11ee81 100644 --- a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts @@ -23,7 +23,7 @@ import { CollectionHelper } from '../../shared/collection/CollectionHelper' const DRAFT_PARAM = DatasetNonNumericVersion.DRAFT import { VersionUpdateType } from '../../../../src/dataset/domain/models/VersionUpdateType' import { ApiConfig } from '@iqss/dataverse-client-javascript' -import { DATAVERSE_BACKEND_URL } from '@/config' +import { DATAVERSE_BACKEND_URL, OIDC_AUTH_CONFIG } from '@/config' import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' chai.use(chaiAsPromised) @@ -163,7 +163,12 @@ describe('Dataset JSDataverse Repository', () => { // Change the api config to use bearer token cy.wrap( - ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) ) await datasetRepository @@ -197,7 +202,12 @@ describe('Dataset JSDataverse Repository', () => { // Change the api config to use bearer token cy.wrap( - ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) ) await datasetRepository @@ -248,7 +258,12 @@ describe('Dataset JSDataverse Repository', () => { // Change the api config to use bearer token cy.wrap( - ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) ) await datasetRepository @@ -288,7 +303,12 @@ describe('Dataset JSDataverse Repository', () => { // Change the api config to use bearer token cy.wrap( - ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) ) await datasetRepository @@ -310,7 +330,12 @@ describe('Dataset JSDataverse Repository', () => { // Change the api config to use bearer token cy.wrap( - ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) ) await datasetRepository.getByPrivateUrlToken(privateUrlResponse.token).then((dataset) => { @@ -335,7 +360,12 @@ describe('Dataset JSDataverse Repository', () => { // Change the api config to use bearer token cy.wrap( - ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) ) await datasetRepository @@ -361,7 +391,12 @@ describe('Dataset JSDataverse Repository', () => { // Change the api config to use bearer token cy.wrap( - ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) ) return datasetRepository @@ -388,7 +423,12 @@ describe('Dataset JSDataverse Repository', () => { // Change the api config to use bearer token cy.wrap( - ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) ) await datasetRepository.getByPersistentId(datasetResponse.persistentId).then((dataset) => { @@ -406,7 +446,12 @@ describe('Dataset JSDataverse Repository', () => { // Change the api config to use bearer token cy.wrap( - ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) ) await datasetRepository @@ -430,7 +475,12 @@ describe('Dataset JSDataverse Repository', () => { it('creates a new dataset from DatasetDTO', async () => { // Change the api config to use bearer token cy.wrap( - ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) ) const datasetDTO: DatasetDTO = { @@ -470,7 +520,12 @@ describe('Dataset JSDataverse Repository', () => { // Change the api config to use bearer token cy.wrap( - ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.BEARER_TOKEN) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) ) await datasetRepository.publish(datasetResponse.persistentId).then((response) => { diff --git a/tests/support/commands.tsx b/tests/support/commands.tsx index 3f76c8c15..bce7fc521 100644 --- a/tests/support/commands.tsx +++ b/tests/support/commands.tsx @@ -48,6 +48,7 @@ import { SessionProvider } from '../../src/sections/session/SessionProvider' import { MemoryRouter } from 'react-router-dom' import { TestsUtils } from '@tests/e2e-integration/shared/TestsUtils' import { Utils } from '@/shared/helpers/Utils' +import { OIDC_AUTH_CONFIG } from '@/config' // Define your custom mount function @@ -88,7 +89,9 @@ Cypress.Commands.add('login', () => { cy.url() .should('eq', `${Cypress.config().baseUrl as string}/spa`) .then(() => { - const token = Utils.getLocalStorageItem('ROCP_token') + const token = Utils.getLocalStorageItem( + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}_token` + ) return cy.wrap(token) }) From ee1c82a128e7d899cfea01dbb03dc5b7287375be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 25 Nov 2024 15:48:53 -0300 Subject: [PATCH 36/97] feat: read error handler --- .../helpers/JSDataverseReadErrorHandler.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/shared/helpers/JSDataverseReadErrorHandler.ts diff --git a/src/shared/helpers/JSDataverseReadErrorHandler.ts b/src/shared/helpers/JSDataverseReadErrorHandler.ts new file mode 100644 index 000000000..530992b86 --- /dev/null +++ b/src/shared/helpers/JSDataverseReadErrorHandler.ts @@ -0,0 +1,36 @@ +import { ReadError } from '@iqss/dataverse-client-javascript' + +export class JSDataverseReadErrorHandler { + private error: ReadError + + constructor(error: ReadError) { + this.error = error + } + + public getErrorMessage(): string { + return this.error.message + } + + public getReason(): string | null { + // Reason comes after "Reason was: " + const reasonMatch = this.error.message.match(/Reason was: (.*)/) + return reasonMatch ? reasonMatch[1] : null + } + + public getStatusCode(): number | null { + // Status code comes inside [] brackets + const statusCodeMatch = this.error.message.match(/\[(\d+)\]/) + return statusCodeMatch ? parseInt(statusCodeMatch[1]) : null + } + + public getReasonWithoutStatusCode(): string | null { + const reason = this.getReason() + if (!reason) return null + + const statusCode = this.getStatusCode() + if (statusCode === null) return reason + + // Remove status code from reason + return reason.replace(`[${statusCode}]`, '').trim() + } +} From 1aa60649c683528524beb710316d6e554e1138e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 25 Nov 2024 16:19:34 -0300 Subject: [PATCH 37/97] feat: detect specific error to redirect user to sign up page --- src/sections/session/SessionProvider.tsx | 14 ++++++++++++-- .../helpers/JSDataverseReadErrorHandler.ts | 18 ++++++++++++++++++ .../repositories/UserJSDataverseRepository.ts | 2 +- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/sections/session/SessionProvider.tsx b/src/sections/session/SessionProvider.tsx index b53b71ecc..50aae49b2 100644 --- a/src/sections/session/SessionProvider.tsx +++ b/src/sections/session/SessionProvider.tsx @@ -5,6 +5,10 @@ import { SessionContext } from './SessionContext' import { getUser } from '../../users/domain/useCases/getUser' import { UserRepository } from '../../users/domain/repositories/UserRepository' import { logOut } from '../../users/domain/useCases/logOut' +import { JSDataverseReadErrorHandler } from '@/shared/helpers/JSDataverseReadErrorHandler' + +export const BEARER_TOKEN_IS_VALID_BUT_NOT_LINKED_MESSAGE = + 'Bearer token is validated, but there is no linked user account.' interface SessionProviderProps { repository: UserRepository @@ -21,8 +25,14 @@ export function SessionProvider({ repository, children }: PropsWithChildren { - throw new Error(error.message) + throw error }) } From c769dfa88b27c736212ce28af4d522914254a7b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 25 Nov 2024 17:13:41 -0300 Subject: [PATCH 38/97] feat: session provider in router, redirect to signup page --- src/App.tsx | 8 +- src/router/routes.tsx | 152 ++++++++++-------- src/sections/Route.enum.ts | 7 +- .../access-file-menu/RequestAccessModal.tsx | 2 +- src/sections/session/SessionProvider.tsx | 20 ++- src/sections/sign-up/SignUp.tsx | 19 +++ src/sections/sign-up/SignUpFactory.tsx | 18 +++ .../RequestAccessModal.spec.tsx | 4 +- 8 files changed, 145 insertions(+), 85 deletions(-) create mode 100644 src/sections/sign-up/SignUp.tsx create mode 100644 src/sections/sign-up/SignUpFactory.tsx diff --git a/src/App.tsx b/src/App.tsx index 4e4c92be9..2096af05f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,8 +2,6 @@ import { AuthProvider, TAuthConfig } from 'react-oauth2-code-pkce' import { ApiConfig } from '@iqss/dataverse-client-javascript/dist/core' import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' import { Router } from './router' -import { SessionProvider } from './sections/session/SessionProvider' -import { UserJSDataverseRepository } from './users/infrastructure/repositories/UserJSDataverseRepository' import { Route } from './sections/Route.enum' import { OIDC_AUTH_CONFIG, DATAVERSE_BACKEND_URL } from './config' import 'react-loading-skeleton/dist/skeleton.css' @@ -35,14 +33,10 @@ const authConfig: TAuthConfig = { storageKeyPrefix: OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX } -const userRepository = new UserJSDataverseRepository() - function App() { return ( - - - + ) } diff --git a/src/router/routes.tsx b/src/router/routes.tsx index 9f381f00d..6ca7927ff 100644 --- a/src/router/routes.tsx +++ b/src/router/routes.tsx @@ -6,6 +6,10 @@ import { ErrorPage } from '../sections/error-page/ErrorPage' import { ProtectedRoute } from './ProtectedRoute' import { AuthCallback } from '../sections/auth-callback/AuthCallback' import { AppLoader } from '../sections/shared/layout/app-loader/AppLoader' +import { SessionProvider } from '@/sections/session/SessionProvider' +import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository' + +const userRepository = new UserJSDataverseRepository() const Homepage = lazy(() => import('../sections/homepage/HomepageFactory').then(({ HomepageFactory }) => ({ @@ -67,109 +71,129 @@ const AccountPage = lazy(() => })) ) +const SignUpPage = lazy(() => + import('../sections/sign-up/SignUpFactory').then(({ SignUpFactory }) => ({ + default: () => SignUpFactory.create() + })) +) + export const routes: RouteObject[] = [ { - path: '/', - element: , - errorElement: , + element: , children: [ { - path: Route.HOME, - element: ( - }> - - - ), - errorElement: - }, - { - path: Route.COLLECTIONS_BASE, - element: ( - }> - - - ), - errorElement: - }, - { - path: Route.COLLECTIONS, - element: ( - }> - - - ), - errorElement: - }, - { - path: Route.DATASETS, - element: ( - }> - - - ), - errorElement: - }, - { - path: Route.FILES, - element: ( - }> - - - ), - errorElement: - }, - { - path: Route.AUTH_CALLBACK, - element: - }, - // 🔐 Protected routes are only accessible to authenticated users - { - element: , + path: '/', + element: , + errorElement: , children: [ { - path: Route.CREATE_COLLECTION, + path: Route.HOME, element: ( }> - + ), errorElement: }, { - path: Route.CREATE_DATASET, + path: Route.COLLECTIONS_BASE, element: ( }> - + ), errorElement: }, { - path: Route.UPLOAD_DATASET_FILES, + path: Route.COLLECTIONS, element: ( }> - + ), errorElement: }, { - path: Route.EDIT_DATASET_METADATA, + path: Route.DATASETS, element: ( }> - + ), errorElement: }, { - path: Route.ACCOUNT, + path: Route.FILES, element: ( }> - + ), errorElement: + }, + { + path: Route.AUTH_CALLBACK, + element: + }, + { + path: Route.SIGN_UP, + element: ( + }> + + + ), + errorElement: + }, + // 🔐 Protected routes are only accessible to authenticated users + { + element: , + children: [ + { + path: Route.CREATE_COLLECTION, + element: ( + }> + + + ), + errorElement: + }, + { + path: Route.CREATE_DATASET, + element: ( + }> + + + ), + errorElement: + }, + { + path: Route.UPLOAD_DATASET_FILES, + element: ( + }> + + + ), + errorElement: + }, + { + path: Route.EDIT_DATASET_METADATA, + element: ( + }> + + + ), + errorElement: + }, + { + path: Route.ACCOUNT, + element: ( + }> + + + ), + errorElement: + } + ] } ] } diff --git a/src/sections/Route.enum.ts b/src/sections/Route.enum.ts index e53b8ea93..8456bbeb0 100644 --- a/src/sections/Route.enum.ts +++ b/src/sections/Route.enum.ts @@ -2,8 +2,8 @@ import { ROOT_COLLECTION_ALIAS } from '../collection/domain/models/Collection' export enum Route { HOME = '/', - SIGN_UP = '/dataverseuser.xhtml?editMode=CREATE&redirectPage=%2Fdataverse.xhtml', - LOG_IN = '/loginpage.xhtml?redirectPage=%2Fdataverse.xhtml', + SIGN_UP_JSF = '/dataverseuser.xhtml?editMode=CREATE&redirectPage=%2Fdataverse.xhtml', + LOG_IN_JSF = '/loginpage.xhtml?redirectPage=%2Fdataverse.xhtml', LOG_OUT = '/', DATASETS = '/datasets', CREATE_DATASET = '/datasets/:collectionId/create', @@ -14,7 +14,8 @@ export enum Route { COLLECTIONS = '/collections/:collectionId', CREATE_COLLECTION = '/collections/:ownerCollectionId/create', ACCOUNT = '/account', - AUTH_CALLBACK = '/auth-callback' + AUTH_CALLBACK = '/auth-callback', + SIGN_UP = '/sign-up' } export const RouteWithParams = { diff --git a/src/sections/file/file-action-buttons/access-file-menu/RequestAccessModal.tsx b/src/sections/file/file-action-buttons/access-file-menu/RequestAccessModal.tsx index 8deb588b5..7e44b4ad3 100644 --- a/src/sections/file/file-action-buttons/access-file-menu/RequestAccessModal.tsx +++ b/src/sections/file/file-action-buttons/access-file-menu/RequestAccessModal.tsx @@ -89,7 +89,7 @@ const RequestAccessLoginMessage = ({ handleClose }: { handleClose: () => void }) <>

- You need to Sign Up or{' '} + You need to Sign Up or{' '} + + + + + + + ) +} diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx new file mode 100644 index 000000000..c4c1cb6c8 --- /dev/null +++ b/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx @@ -0,0 +1,49 @@ +import { useContext } from 'react' +import { AuthContext } from 'react-oauth2-code-pkce' +import { OIDC_STANDARD_CLAIMS, type ValidTokenNotLinkedAccountFormData } from './types' +import { FormFields } from './FormFields' +import { ValidTokenNotLinkedAccountFormHelper } from './ValidTokenNotLinkedAccountFormHelper' + +export const ValidTokenNotLinkedAccountForm = () => { + const { tokenData } = useContext(AuthContext) + + const defaultUserName = + ValidTokenNotLinkedAccountFormHelper.getTokenDataValue( + OIDC_STANDARD_CLAIMS.PREFERRED_USERNAME, + 'string', + tokenData + ) ?? '' + + const defaultFirstName = + ValidTokenNotLinkedAccountFormHelper.getTokenDataValue( + OIDC_STANDARD_CLAIMS.GIVEN_NAME, + 'string', + tokenData + ) ?? '' + + const defaultLastName = + ValidTokenNotLinkedAccountFormHelper.getTokenDataValue( + OIDC_STANDARD_CLAIMS.FAMILY_NAME, + 'string', + tokenData + ) ?? '' + + const defaultEmail = + ValidTokenNotLinkedAccountFormHelper.getTokenDataValue( + OIDC_STANDARD_CLAIMS.EMAIL, + 'string', + tokenData + ) ?? '' + + const formDefaultValues: ValidTokenNotLinkedAccountFormData = { + username: defaultUserName, + firstName: defaultFirstName, + lastName: defaultLastName, + emailAddress: defaultEmail, + position: '', + affiliation: '', + termsAccepted: false + } + + return +} diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountFormHelper.ts b/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountFormHelper.ts new file mode 100644 index 000000000..2edd04a86 --- /dev/null +++ b/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountFormHelper.ts @@ -0,0 +1,88 @@ +import { type TTokenData } from 'react-oauth2-code-pkce/dist/types' +import { + OIDC_STANDARD_CLAIMS, + OptionalExceptFor, + ValidTokenNotLinkedAccountFormData +} from './types' + +export class ValidTokenNotLinkedAccountFormHelper { + public static getTokenDataValue( + key: string, + expectedType: 'string' | 'number' | 'boolean' | 'object', + tokenData?: TTokenData + ): T | undefined { + if (!tokenData) { + return undefined + } + + if (!(key in tokenData)) { + return undefined + } + + const value = tokenData[key] as unknown + + if (typeof value !== expectedType) { + console.error( + `Expected token data key: ${key} to be of type ${expectedType} but got ${typeof value}` + ) + return undefined + } + + return value as T + } + + public static defineRegistrationDTOProperties( + defaultValues: ValidTokenNotLinkedAccountFormData, + formData: ValidTokenNotLinkedAccountFormData, + tokenData?: TTokenData + ) { + const registrationDTO: OptionalExceptFor = + { + termsAccepted: formData.termsAccepted + } + + // This will be a weird scenario, not having tokenData from the access token + if (!tokenData) { + registrationDTO.username = formData.username + registrationDTO.firstName = formData.firstName + registrationDTO.lastName = formData.lastName + registrationDTO.emailAddress = formData.emailAddress + registrationDTO.position = formData.position + registrationDTO.affiliation = formData.affiliation + + return registrationDTO + } + + // If properties are in the tokenData then dont send them at all + + if (OIDC_STANDARD_CLAIMS.PREFERRED_USERNAME in tokenData === false) { + console.log('preferred_username not in tokenData') + registrationDTO.username = formData.username + } + + if (OIDC_STANDARD_CLAIMS.GIVEN_NAME in tokenData === false) { + console.log('given_name not in tokenData') + registrationDTO.firstName = formData.firstName + } + + if (OIDC_STANDARD_CLAIMS.FAMILY_NAME in tokenData === false) { + console.log('family_name not in tokenData') + registrationDTO.lastName = formData.lastName + } + + if (OIDC_STANDARD_CLAIMS.EMAIL in tokenData === false) { + console.log('email not in tokenData') + registrationDTO.emailAddress = formData.emailAddress + } + + if (formData.affiliation) { + registrationDTO.affiliation = formData.affiliation + } + + if (formData.position) { + registrationDTO.position = formData.position + } + + return registrationDTO + } +} diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/types.ts b/src/sections/sign-up/valid-token-not-linked-account-form/types.ts new file mode 100644 index 000000000..ff27efc62 --- /dev/null +++ b/src/sections/sign-up/valid-token-not-linked-account-form/types.ts @@ -0,0 +1,20 @@ +export interface ValidTokenNotLinkedAccountFormData { + username: string + firstName: string + lastName: string + emailAddress: string + position: string + affiliation: string + termsAccepted: boolean +} + +// This enum is based only on some of the standard claims according to the official openId documentation https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + +export enum OIDC_STANDARD_CLAIMS { + GIVEN_NAME = 'given_name', + FAMILY_NAME = 'family_name', + PREFERRED_USERNAME = 'preferred_username', + EMAIL = 'email' +} + +export type OptionalExceptFor = Partial & Pick diff --git a/src/shared/helpers/Validator.ts b/src/shared/helpers/Validator.ts index bdbc8e6fb..43a1a4546 100644 --- a/src/shared/helpers/Validator.ts +++ b/src/shared/helpers/Validator.ts @@ -9,4 +9,10 @@ export class Validator { const IDENTIFIER_REGEX = /^[a-zA-Z0-9_-]+$/ return IDENTIFIER_REGEX.test(input) } + + static isValidUsername(input: string): boolean { + const USERNAME_REGEX = /^[a-zA-Z0-9_.-]{2,60}$/ + + return USERNAME_REGEX.test(input) + } } From 59a47e2924f6a539332f85b0cdd13ac51e81ffac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 26 Nov 2024 15:48:28 -0300 Subject: [PATCH 40/97] feat(design-system): add rows prop --- packages/design-system/CHANGELOG.md | 1 + .../form/form-group/form-element/FormTextArea.tsx | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/design-system/CHANGELOG.md b/packages/design-system/CHANGELOG.md index 330ffddb8..897311d36 100644 --- a/packages/design-system/CHANGELOG.md +++ b/packages/design-system/CHANGELOG.md @@ -54,6 +54,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline - **Offcanvas:** NEW Offcanvas component. - **FormCheckbox:** modify Props Interface to allow any react node as `label` prop. - **RichTextEditor:** NEW Rich Text Editor component. +- **FormTextArea:** modify Props Interface to allow `rows` prop. # [1.1.0](https://github.com/IQSS/dataverse-frontend/compare/@iqss/dataverse-design-system@1.0.1...@iqss/dataverse-design-system@1.1.0) (2024-03-12) diff --git a/packages/design-system/src/lib/components/form/form-group/form-element/FormTextArea.tsx b/packages/design-system/src/lib/components/form/form-group/form-element/FormTextArea.tsx index b18448dc5..6292a32c5 100644 --- a/packages/design-system/src/lib/components/form/form-group/form-element/FormTextArea.tsx +++ b/packages/design-system/src/lib/components/form/form-group/form-element/FormTextArea.tsx @@ -2,23 +2,24 @@ import { Form as FormBS } from 'react-bootstrap' import * as React from 'react' export type FormInputElement = HTMLInputElement | HTMLTextAreaElement -export interface FormTextAreaProps extends Omit, 'rows'> { +export interface FormTextAreaProps extends React.HTMLAttributes { name?: string disabled?: boolean isValid?: boolean isInvalid?: boolean value?: string autoFocus?: boolean + rows?: number } export const FormTextArea = React.forwardRef(function FormTextArea( - { name, disabled, isValid, isInvalid, value, autoFocus, ...props }: FormTextAreaProps, + { name, disabled, isValid, isInvalid, value, autoFocus, rows = 5, ...props }: FormTextAreaProps, ref ) { return ( Date: Tue, 26 Nov 2024 22:41:33 -0300 Subject: [PATCH 41/97] feat: add alert about pre-filled fields and msg --- src/sections/sign-up/SignUp.tsx | 19 ++++++++++++------- .../FormFields.module.scss | 4 ++++ .../FormFields.tsx | 18 ++++++++++-------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/sections/sign-up/SignUp.tsx b/src/sections/sign-up/SignUp.tsx index bbb0273d6..1a82c6b58 100644 --- a/src/sections/sign-up/SignUp.tsx +++ b/src/sections/sign-up/SignUp.tsx @@ -6,8 +6,8 @@ import { useLoading } from '../loading/LoadingContext' import { ValidTokenNotLinkedAccountForm } from './valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm' // const collectionRepository = new CollectionJSDataverseRepository() -// // TODO:ME- All use cases will return same error message so we can use anyone? -// const { collection } = useCollection(collectionRepository, ':root') +// TODO:ME- All use cases will return same error message so this is blocking us for making requests to other public use cases like get root collection +// const { collection } = useCollection(collectionRepository, ':root') interface SignUpProps { hasValidTokenButNotLinkedAccount: boolean @@ -28,11 +28,16 @@ export const SignUp = ({ hasValidTokenButNotLinkedAccount }: SignUpProps) => { )} {hasValidTokenButNotLinkedAccount && ( - - - {t('hasValidTokenButNotLinkedAccount.alertText')} - - + <> + + + {t('hasValidTokenButNotLinkedAccount.alertText')} + + + + {t('aboutPrefilledFields')} + + )}

diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.module.scss b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.module.scss index 4cc40e620..ff7c48956 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.module.scss +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.module.scss @@ -16,4 +16,8 @@ max-width: 100ch; margin-bottom: 1rem; white-space: pre-wrap; + + svg { + translate: 0 -15%; + } } diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx index 348416cb1..559df44d3 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx @@ -1,14 +1,14 @@ -import { axiosInstance } from '@/axiosInstance' import { AxiosError } from 'axios' +import { axiosInstance } from '@/axiosInstance' import { Controller, FormProvider, useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' -import { Button, Col, Form, Stack } from '@iqss/dataverse-design-system' -import { TTokenData } from 'react-oauth2-code-pkce/dist/types' -import { ValidTokenNotLinkedAccountFormData } from './types' -import styles from './FormFields.module.scss' import { useNavigate } from 'react-router-dom' +import { TTokenData } from 'react-oauth2-code-pkce/dist/types' +import { Button, Col, Form, Stack } from '@iqss/dataverse-design-system' import { Validator } from '@/shared/helpers/Validator' +import { ValidTokenNotLinkedAccountFormData } from './types' import { ValidTokenNotLinkedAccountFormHelper } from './ValidTokenNotLinkedAccountFormHelper' +import styles from './FormFields.module.scss' interface FormFieldsProps { formDefaultValues: ValidTokenNotLinkedAccountFormData @@ -96,9 +96,11 @@ export const FormFields = ({ formDefaultValues, tokenData }: FormFieldsProps) => return (
-
- {t('aboutPrefilledFields')} -
+ {/*
+ + {t('aboutPrefilledFields')} + +
*/}
Date: Wed, 27 Nov 2024 00:15:41 -0300 Subject: [PATCH 42/97] feat: catch errors and navigate after success --- src/sections/session/SessionContext.ts | 11 +- src/sections/session/SessionProvider.tsx | 169 ++++++++++++++---- src/sections/sign-up/SignUp.tsx | 9 +- .../FormFields.tsx | 31 +++- .../ValidTokenNotLinkedAccountFormHelper.ts | 4 - .../helpers/JSDataverseReadErrorHandler.ts | 17 +- 6 files changed, 181 insertions(+), 60 deletions(-) diff --git a/src/sections/session/SessionContext.ts b/src/sections/session/SessionContext.ts index 6cea96da8..3bea4620a 100644 --- a/src/sections/session/SessionContext.ts +++ b/src/sections/session/SessionContext.ts @@ -4,14 +4,23 @@ import { User } from '../../users/domain/models/User' interface SessionContextProps { user: User | null isLoadingUser: boolean + sessionError: SessionError | null setUser: (user: User) => void logout: () => Promise + refetchUserSession: () => Promise } export const SessionContext = createContext({ user: null, isLoadingUser: true, + sessionError: null, setUser: /* istanbul ignore next */ () => {}, - logout: /* istanbul ignore next */ () => Promise.resolve() + logout: /* istanbul ignore next */ () => Promise.resolve(), + refetchUserSession: /* istanbul ignore next */ () => Promise.resolve() }) export const useSession = () => useContext(SessionContext) + +export interface SessionError { + statusCode: number | null + message: string +} diff --git a/src/sections/session/SessionProvider.tsx b/src/sections/session/SessionProvider.tsx index a6f5a2cce..c929eccd4 100644 --- a/src/sections/session/SessionProvider.tsx +++ b/src/sections/session/SessionProvider.tsx @@ -1,13 +1,14 @@ import { Outlet, useNavigate } from 'react-router-dom' -import { useContext, useEffect, useState } from 'react' +import { useCallback, useContext, useEffect, useState } from 'react' import { AuthContext } from 'react-oauth2-code-pkce' import { User } from '../../users/domain/models/User' -import { SessionContext } from './SessionContext' +import { SessionContext, SessionError } from './SessionContext' import { getUser } from '../../users/domain/useCases/getUser' import { UserRepository } from '../../users/domain/repositories/UserRepository' import { logOut } from '../../users/domain/useCases/logOut' import { JSDataverseReadErrorHandler } from '@/shared/helpers/JSDataverseReadErrorHandler' import { QueryParamKey, Route } from '../Route.enum' +import { ReadError } from '@iqss/dataverse-client-javascript' export const BEARER_TOKEN_IS_VALID_BUT_NOT_LINKED_MESSAGE = 'Bearer token is validated, but there is no linked user account.' @@ -15,49 +16,153 @@ export const BEARER_TOKEN_IS_VALID_BUT_NOT_LINKED_MESSAGE = interface SessionProviderProps { repository: UserRepository } + export function SessionProvider({ repository }: SessionProviderProps) { + const navigate = useNavigate() const { token, loginInProgress } = useContext(AuthContext) const [user, setUser] = useState(null) - const [isLoadingUser, setIsLoadingUser] = useState(false) - const navigate = useNavigate() + const [isLoadingUser, setIsLoadingUser] = useState(false) + const [sessionError, setSessionError] = useState(null) - useEffect(() => { - const handleGetUser = async () => { - setIsLoadingUser(true) - try { - const user: User = await getUser(repository) - - user && setUser(user) - } catch (err: unknown) { - if (JSDataverseReadErrorHandler.isBearerTokenValidatedButNoLinkedUserAccountError(err)) { - const searchParams = new URLSearchParams() - searchParams.set(QueryParamKey.VALID_TOKEN_BUT_NOT_LINKED_ACCOUNT, 'true') - - navigate(`${Route.SIGN_UP}?${searchParams.toString()}`) + // TODO:ME - Ask how to handle when user doesn't want to sign up, save in memory to avoid redirecting it again. Next time ask with an alert? + const handleFetchError = useCallback( + (err: unknown) => { + if (err instanceof ReadError) { + const readErrorHandler = new JSDataverseReadErrorHandler(err) + const statusCode = readErrorHandler.getStatusCode() + const errorMessage = + readErrorHandler.getReasonWithoutStatusCode() ?? readErrorHandler.getErrorMessage() + + // Handle specific error: Bearer token validated, but no linked user account + if (readErrorHandler.isBearerTokenValidatedButNoLinkedUserAccountError()) { + setSessionError({ statusCode, message: errorMessage }) + + // Redirect to the sign-up page with a query param + navigate( + `${Route.SIGN_UP}?${new URLSearchParams({ + [QueryParamKey.VALID_TOKEN_BUT_NOT_LINKED_ACCOUNT]: 'true' + }).toString()}` + ) + + return } - // TODO:ME - Handle another type of error - // TODO:ME - Ask how to handle when user doesn't want to sign up, save in memory to avoid redirecting it again. Next time ask with an alert? - } finally { - setIsLoadingUser(false) + + // Set session error for other ReadError cases + setSessionError({ statusCode, message: errorMessage }) + return } - } - if (token && !loginInProgress) { - void handleGetUser() + // Handle unexpected errors + setSessionError({ + statusCode: null, + message: 'An unexpected error occurred while getting the user.' + }) + }, + [navigate] + ) + + const fetchUser = useCallback(async () => { + setIsLoadingUser(true) + + try { + const user = await getUser(repository) + setUser(user) + } catch (err) { + handleFetchError(err) + } finally { + setIsLoadingUser(false) } - }, [repository, token, loginInProgress, navigate]) + }, [repository, handleFetchError]) - const submitLogOut = () => { - return logOut(repository) - .then(() => { - setUser(null) - }) - .catch((error) => console.error('There was an error logging out the user', error)) + const refetchUserSession = async () => { + await fetchUser() } + const submitLogOut = async () => { + try { + await logOut(repository) + setUser(null) + } catch (error) { + console.error('Error logging out:', error) + } + } + + useEffect(() => { + if (token && !loginInProgress) { + void fetchUser() + } + }, [repository, token, loginInProgress, navigate, fetchUser]) + return ( - + ) } + +// export function SessionProvider({ repository }: SessionProviderProps) { +// const { token, loginInProgress } = useContext(AuthContext) +// const [user, setUser] = useState(null) +// const [isLoadingUser, setIsLoadingUser] = useState(false) +// const navigate = useNavigate() + +// useEffect(() => { +// const handleGetUser = async () => { +// setIsLoadingUser(true) +// try { +// const user: User = await getUser(repository) + +// user && setUser(user) +// } catch (err: unknown) { +// if (JSDataverseReadErrorHandler.isBearerTokenValidatedButNoLinkedUserAccountError(err)) { +// const searchParams = new URLSearchParams() +// searchParams.set(QueryParamKey.VALID_TOKEN_BUT_NOT_LINKED_ACCOUNT, 'true') + +// navigate(`${Route.SIGN_UP}?${searchParams.toString()}`) +// } +// // TODO:ME - Handle another type of error +// // TODO:ME - Ask how to handle when user doesn't want to sign up, save in memory to avoid redirecting it again. Next time ask with an alert? +// } finally { +// setIsLoadingUser(false) +// } +// } + +// if (token && !loginInProgress) { +// void handleGetUser() +// } +// }, [repository, token, loginInProgress, navigate]) + +// const refetchUser = () => { +// setIsLoadingUser(true) + +// getUser(repository) +// .then((user) => { +// setUser(user) +// }) +// .catch((error) => console.error('There was an error fetching the user', error)) +// .finally(() => setIsLoadingUser(false)) +// } + +// const submitLogOut = () => { +// return logOut(repository) +// .then(() => { +// setUser(null) +// }) +// .catch((error) => console.error('There was an error logging out the user', error)) +// } + +// return ( +// +// +// +// ) +// } diff --git a/src/sections/sign-up/SignUp.tsx b/src/sections/sign-up/SignUp.tsx index 1a82c6b58..96cefcc1f 100644 --- a/src/sections/sign-up/SignUp.tsx +++ b/src/sections/sign-up/SignUp.tsx @@ -1,13 +1,12 @@ +import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Alert, Tabs } from '@iqss/dataverse-design-system' -import styles from './SignUp.module.scss' -import { useEffect } from 'react' import { useLoading } from '../loading/LoadingContext' import { ValidTokenNotLinkedAccountForm } from './valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm' +import styles from './SignUp.module.scss' -// const collectionRepository = new CollectionJSDataverseRepository() -// TODO:ME- All use cases will return same error message so this is blocking us for making requests to other public use cases like get root collection -// const { collection } = useCollection(collectionRepository, ':root') +// TODO:ME - All use cases will return same error message so this is blocking us for making requests to other public use cases like get root collection, should work removing access token from localstorage but we need it for future call +// TODO:ME - How to handle 401 Unauthorized {"status":"ERROR","message":"Unauthorized bearer token."} globally, maybe redirect to oidc login page? interface SignUpProps { hasValidTokenButNotLinkedAccount: boolean diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx index 559df44d3..ac15e5665 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx @@ -5,8 +5,9 @@ import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { TTokenData } from 'react-oauth2-code-pkce/dist/types' import { Button, Col, Form, Stack } from '@iqss/dataverse-design-system' +import { useSession } from '@/sections/session/SessionContext' import { Validator } from '@/shared/helpers/Validator' -import { ValidTokenNotLinkedAccountFormData } from './types' +import { type ValidTokenNotLinkedAccountFormData } from './types' import { ValidTokenNotLinkedAccountFormHelper } from './ValidTokenNotLinkedAccountFormHelper' import styles from './FormFields.module.scss' @@ -15,12 +16,30 @@ interface FormFieldsProps { tokenData: TTokenData | undefined } +// TODO:ME - Maybe we should redirect to a welcome page after success? ask if there is one, maybe not the case for this scenario +// TODO:ME - We will need an api call to get the terms of use of the installation +// TODO:ME - Show the registration write error message to the user after encapsulating this call in js-dataverse + +/* + This is the expected response from the server after succesfull registration, will help for js-dataverse-client-javascript + const resp = { + data: { + status: 'OK', + data: { + message: 'User registered.' + } + }, + status: 200, + statusText: 'OK' + } +*/ + export const FormFields = ({ formDefaultValues, tokenData }: FormFieldsProps) => { const navigate = useNavigate() + const { refetchUserSession } = useSession() const { t } = useTranslation('signUp') const { t: tShared } = useTranslation('shared') - // TODO:ME - We will need an api call to get the terms of use of the installation const hasTermsOfUse = false const isUsernameRequired = formDefaultValues.username === '' @@ -41,14 +60,12 @@ export const FormFields = ({ formDefaultValues, tokenData }: FormFieldsProps) => tokenData ) - console.log({ registrationDTO }) - // curl -H "Authorization: Bearer $TOKEN" -X POST http://localhost:8080/api/users/register --data '{"termsAccepted":true}' axiosInstance .post('/api/users/register', registrationDTO) - .then((response) => { - console.log({ response }) + .then(async () => { + await refetchUserSession() - // TODO:ME - Enforce the SessionProvider to make a new call to get the user now and navigate to the root collection + navigate('/') }) .catch((error: AxiosError) => { console.error({ error }) diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountFormHelper.ts b/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountFormHelper.ts index 2edd04a86..5e9e24894 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountFormHelper.ts +++ b/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountFormHelper.ts @@ -56,22 +56,18 @@ export class ValidTokenNotLinkedAccountFormHelper { // If properties are in the tokenData then dont send them at all if (OIDC_STANDARD_CLAIMS.PREFERRED_USERNAME in tokenData === false) { - console.log('preferred_username not in tokenData') registrationDTO.username = formData.username } if (OIDC_STANDARD_CLAIMS.GIVEN_NAME in tokenData === false) { - console.log('given_name not in tokenData') registrationDTO.firstName = formData.firstName } if (OIDC_STANDARD_CLAIMS.FAMILY_NAME in tokenData === false) { - console.log('family_name not in tokenData') registrationDTO.lastName = formData.lastName } if (OIDC_STANDARD_CLAIMS.EMAIL in tokenData === false) { - console.log('email not in tokenData') registrationDTO.emailAddress = formData.emailAddress } diff --git a/src/shared/helpers/JSDataverseReadErrorHandler.ts b/src/shared/helpers/JSDataverseReadErrorHandler.ts index d8a17ec0a..27ecf1ecc 100644 --- a/src/shared/helpers/JSDataverseReadErrorHandler.ts +++ b/src/shared/helpers/JSDataverseReadErrorHandler.ts @@ -35,20 +35,15 @@ export class JSDataverseReadErrorHandler { return reason.replace(`[${statusCode}]`, '').trim() } - public static isBearerTokenValidatedButNoLinkedUserAccountError(err: unknown): boolean { - if (err instanceof ReadError) { - const errorHandler = new JSDataverseReadErrorHandler(err) + public isBearerTokenValidatedButNoLinkedUserAccountError(): boolean { + const formattedError: string = this.getReasonWithoutStatusCode() ?? this.getErrorMessage() - const formattedError: string = - errorHandler.getReasonWithoutStatusCode() ?? errorHandler.getErrorMessage() + const statusCode: number | null = this.getStatusCode() - const statusCode: number | null = errorHandler.getStatusCode() - - if (statusCode === 403 && formattedError === BEARER_TOKEN_IS_VALID_BUT_NOT_LINKED_MESSAGE) { - return true // Return true if the specific error is detected - } + if (statusCode === 403 && formattedError === BEARER_TOKEN_IS_VALID_BUT_NOT_LINKED_MESSAGE) { + return true } - return false // Return false if no match + return false } } From f77475c948c2cb21627ffb53a3b28d4caf0e0084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 27 Nov 2024 10:02:39 -0300 Subject: [PATCH 43/97] feat: logout user if cancels the form --- src/sections/session/SessionProvider.tsx | 60 ------------------- .../FormFields.tsx | 21 ++++--- .../ValidTokenNotLinkedAccountForm.tsx | 2 +- .../ValidTokenNotLinkedAccountFormHelper.ts | 1 - .../types.ts | 3 +- 5 files changed, 16 insertions(+), 71 deletions(-) diff --git a/src/sections/session/SessionProvider.tsx b/src/sections/session/SessionProvider.tsx index c929eccd4..02b696193 100644 --- a/src/sections/session/SessionProvider.tsx +++ b/src/sections/session/SessionProvider.tsx @@ -24,7 +24,6 @@ export function SessionProvider({ repository }: SessionProviderProps) { const [isLoadingUser, setIsLoadingUser] = useState(false) const [sessionError, setSessionError] = useState(null) - // TODO:ME - Ask how to handle when user doesn't want to sign up, save in memory to avoid redirecting it again. Next time ask with an alert? const handleFetchError = useCallback( (err: unknown) => { if (err instanceof ReadError) { @@ -107,62 +106,3 @@ export function SessionProvider({ repository }: SessionProviderProps) { ) } - -// export function SessionProvider({ repository }: SessionProviderProps) { -// const { token, loginInProgress } = useContext(AuthContext) -// const [user, setUser] = useState(null) -// const [isLoadingUser, setIsLoadingUser] = useState(false) -// const navigate = useNavigate() - -// useEffect(() => { -// const handleGetUser = async () => { -// setIsLoadingUser(true) -// try { -// const user: User = await getUser(repository) - -// user && setUser(user) -// } catch (err: unknown) { -// if (JSDataverseReadErrorHandler.isBearerTokenValidatedButNoLinkedUserAccountError(err)) { -// const searchParams = new URLSearchParams() -// searchParams.set(QueryParamKey.VALID_TOKEN_BUT_NOT_LINKED_ACCOUNT, 'true') - -// navigate(`${Route.SIGN_UP}?${searchParams.toString()}`) -// } -// // TODO:ME - Handle another type of error -// // TODO:ME - Ask how to handle when user doesn't want to sign up, save in memory to avoid redirecting it again. Next time ask with an alert? -// } finally { -// setIsLoadingUser(false) -// } -// } - -// if (token && !loginInProgress) { -// void handleGetUser() -// } -// }, [repository, token, loginInProgress, navigate]) - -// const refetchUser = () => { -// setIsLoadingUser(true) - -// getUser(repository) -// .then((user) => { -// setUser(user) -// }) -// .catch((error) => console.error('There was an error fetching the user', error)) -// .finally(() => setIsLoadingUser(false)) -// } - -// const submitLogOut = () => { -// return logOut(repository) -// .then(() => { -// setUser(null) -// }) -// .catch((error) => console.error('There was an error logging out the user', error)) -// } - -// return ( -// -// -// -// ) -// } diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx index ac15e5665..f8d6facd6 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx @@ -1,9 +1,10 @@ -import { AxiosError } from 'axios' -import { axiosInstance } from '@/axiosInstance' +import { useContext } from 'react' +import { useNavigate } from 'react-router-dom' +import { AuthContext } from 'react-oauth2-code-pkce' import { Controller, FormProvider, useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' -import { useNavigate } from 'react-router-dom' -import { TTokenData } from 'react-oauth2-code-pkce/dist/types' +import { AxiosError } from 'axios' +import { axiosInstance } from '@/axiosInstance' import { Button, Col, Form, Stack } from '@iqss/dataverse-design-system' import { useSession } from '@/sections/session/SessionContext' import { Validator } from '@/shared/helpers/Validator' @@ -13,12 +14,12 @@ import styles from './FormFields.module.scss' interface FormFieldsProps { formDefaultValues: ValidTokenNotLinkedAccountFormData - tokenData: TTokenData | undefined } // TODO:ME - Maybe we should redirect to a welcome page after success? ask if there is one, maybe not the case for this scenario // TODO:ME - We will need an api call to get the terms of use of the installation // TODO:ME - Show the registration write error message to the user after encapsulating this call in js-dataverse +// TODO:ME - Ask about logout when clicking the Cancel button because of the BEARER_TOKEN_IS_VALID_BUT_NOT_LINKED_MESSAGE error /* This is the expected response from the server after succesfull registration, will help for js-dataverse-client-javascript @@ -34,9 +35,10 @@ interface FormFieldsProps { } */ -export const FormFields = ({ formDefaultValues, tokenData }: FormFieldsProps) => { +export const FormFields = ({ formDefaultValues }: FormFieldsProps) => { const navigate = useNavigate() const { refetchUserSession } = useSession() + const { tokenData, logOut: oidcLogout } = useContext(AuthContext) const { t } = useTranslation('signUp') const { t: tShared } = useTranslation('shared') @@ -55,7 +57,6 @@ export const FormFields = ({ formDefaultValues, tokenData }: FormFieldsProps) => const submitForm = (formData: ValidTokenNotLinkedAccountFormData) => { // We wont send properties that are already present in the tokenData, those are the disabled/readonly fields const registrationDTO = ValidTokenNotLinkedAccountFormHelper.defineRegistrationDTOProperties( - formDefaultValues, formData, tokenData ) @@ -72,6 +73,10 @@ export const FormFields = ({ formDefaultValues, tokenData }: FormFieldsProps) => }) } + // If the user cancels the registration, we should logout the user and redirect to the home page. + // This is to avoid sending the valid bearer token and receiving the same BEARER_TOKEN_IS_VALID_BUT_NOT_LINKED_MESSAGE error + const handleCancel = () => oidcLogout() + const userNameRules = { required: isUsernameRequired ? t('fields.username.required') : false, validate: (value: string) => { @@ -314,7 +319,7 @@ export const FormFields = ({ formDefaultValues, tokenData }: FormFieldsProps) => {t('submit')} - diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx index c4c1cb6c8..cf7aa7b2e 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx @@ -45,5 +45,5 @@ export const ValidTokenNotLinkedAccountForm = () => { termsAccepted: false } - return + return } diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountFormHelper.ts b/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountFormHelper.ts index 5e9e24894..c16ac80bc 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountFormHelper.ts +++ b/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountFormHelper.ts @@ -32,7 +32,6 @@ export class ValidTokenNotLinkedAccountFormHelper { } public static defineRegistrationDTOProperties( - defaultValues: ValidTokenNotLinkedAccountFormData, formData: ValidTokenNotLinkedAccountFormData, tokenData?: TTokenData ) { diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/types.ts b/src/sections/sign-up/valid-token-not-linked-account-form/types.ts index ff27efc62..018fa4cdb 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/types.ts +++ b/src/sections/sign-up/valid-token-not-linked-account-form/types.ts @@ -8,7 +8,8 @@ export interface ValidTokenNotLinkedAccountFormData { termsAccepted: boolean } -// This enum is based only on some of the standard claims according to the official openId documentation https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims +// This enum is based only on some of the standard claims according to the official openId documentation +// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims export enum OIDC_STANDARD_CLAIMS { GIVEN_NAME = 'given_name', From 07bd1cd86554756e39e1edcb68372a14d3498d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 27 Nov 2024 10:40:45 -0300 Subject: [PATCH 44/97] feat: session provider work for unit testing also --- src/sections/session/SessionProvider.tsx | 33 +++++++++++++++++++----- tests/support/commands.tsx | 10 ++++--- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/sections/session/SessionProvider.tsx b/src/sections/session/SessionProvider.tsx index 02b696193..8b86880da 100644 --- a/src/sections/session/SessionProvider.tsx +++ b/src/sections/session/SessionProvider.tsx @@ -1,5 +1,5 @@ import { Outlet, useNavigate } from 'react-router-dom' -import { useCallback, useContext, useEffect, useState } from 'react' +import { ReactNode, useCallback, useContext, useEffect, useState } from 'react' import { AuthContext } from 'react-oauth2-code-pkce' import { User } from '../../users/domain/models/User' import { SessionContext, SessionError } from './SessionContext' @@ -13,11 +13,23 @@ import { ReadError } from '@iqss/dataverse-client-javascript' export const BEARER_TOKEN_IS_VALID_BUT_NOT_LINKED_MESSAGE = 'Bearer token is validated, but there is no linked user account.' -interface SessionProviderProps { - repository: UserRepository -} +type SessionProviderProps = + | { + repository: UserRepository + forComponentTesting?: false + testComponent?: never + } + | { + repository: UserRepository + forComponentTesting: true + testComponent: ReactNode + } -export function SessionProvider({ repository }: SessionProviderProps) { +export function SessionProvider({ + repository, + forComponentTesting, + testComponent +}: SessionProviderProps) { const navigate = useNavigate() const { token, loginInProgress } = useContext(AuthContext) const [user, setUser] = useState(null) @@ -90,7 +102,14 @@ export function SessionProvider({ repository }: SessionProviderProps) { if (token && !loginInProgress) { void fetchUser() } - }, [repository, token, loginInProgress, navigate, fetchUser]) + }, [token, loginInProgress, fetchUser]) + + // This is only for component testing purposes + useEffect(() => { + if (forComponentTesting) { + void fetchUser() + } + }, [fetchUser, forComponentTesting]) return ( - + {!forComponentTesting ? : testComponent} ) } diff --git a/tests/support/commands.tsx b/tests/support/commands.tsx index bce7fc521..1c23b30ea 100644 --- a/tests/support/commands.tsx +++ b/tests/support/commands.tsx @@ -44,11 +44,11 @@ import { ReactNode } from 'react' import { I18nextProvider } from 'react-i18next' import i18next from '../../src/i18n' import { UserRepository } from '../../src/users/domain/repositories/UserRepository' -import { SessionProvider } from '../../src/sections/session/SessionProvider' import { MemoryRouter } from 'react-router-dom' import { TestsUtils } from '@tests/e2e-integration/shared/TestsUtils' import { Utils } from '@/shared/helpers/Utils' import { OIDC_AUTH_CONFIG } from '@/config' +import { SessionProvider } from '@/sections/session/SessionProvider' // Define your custom mount function @@ -68,7 +68,9 @@ Cypress.Commands.add('mountAuthenticated', (component: ReactNode) => { userRepository.getAuthenticated = cy.stub().resolves(user) userRepository.removeAuthenticated = cy.stub().resolves() - return cy.customMount({component}) + return cy.customMount( + + ) }) Cypress.Commands.add('mountSuperuser', (component: ReactNode) => { @@ -77,7 +79,9 @@ Cypress.Commands.add('mountSuperuser', (component: ReactNode) => { userRepository.getAuthenticated = cy.stub().resolves(user) userRepository.removeAuthenticated = cy.stub().resolves() - return cy.customMount({component}) + return cy.customMount( + + ) }) Cypress.Commands.add('login', () => { From 0cfd0174d702520830c4562289aa84fdee2f6d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 27 Nov 2024 11:46:13 -0300 Subject: [PATCH 45/97] test: fix component tests --- src/sections/session/SessionProvider.tsx | 31 ++----- src/stories/WithLoggedInSuperUser.tsx | 4 +- src/stories/WithLoggedInUser.tsx | 4 +- .../component/sections/layout/Layout.spec.tsx | 3 +- .../sections/layout/header/Header.spec.tsx | 80 ++----------------- .../sections/session/useSession.spec.tsx | 63 --------------- tests/support/commands.tsx | 37 +++++---- 7 files changed, 44 insertions(+), 178 deletions(-) delete mode 100644 tests/component/sections/session/useSession.spec.tsx diff --git a/src/sections/session/SessionProvider.tsx b/src/sections/session/SessionProvider.tsx index 8b86880da..25ee29378 100644 --- a/src/sections/session/SessionProvider.tsx +++ b/src/sections/session/SessionProvider.tsx @@ -1,5 +1,5 @@ import { Outlet, useNavigate } from 'react-router-dom' -import { ReactNode, useCallback, useContext, useEffect, useState } from 'react' +import { useCallback, useContext, useEffect, useState } from 'react' import { AuthContext } from 'react-oauth2-code-pkce' import { User } from '../../users/domain/models/User' import { SessionContext, SessionError } from './SessionContext' @@ -13,23 +13,11 @@ import { ReadError } from '@iqss/dataverse-client-javascript' export const BEARER_TOKEN_IS_VALID_BUT_NOT_LINKED_MESSAGE = 'Bearer token is validated, but there is no linked user account.' -type SessionProviderProps = - | { - repository: UserRepository - forComponentTesting?: false - testComponent?: never - } - | { - repository: UserRepository - forComponentTesting: true - testComponent: ReactNode - } +interface SessionProviderProps { + repository: UserRepository +} -export function SessionProvider({ - repository, - forComponentTesting, - testComponent -}: SessionProviderProps) { +export function SessionProvider({ repository }: SessionProviderProps) { const navigate = useNavigate() const { token, loginInProgress } = useContext(AuthContext) const [user, setUser] = useState(null) @@ -104,13 +92,6 @@ export function SessionProvider({ } }, [token, loginInProgress, fetchUser]) - // This is only for component testing purposes - useEffect(() => { - if (forComponentTesting) { - void fetchUser() - } - }, [fetchUser, forComponentTesting]) - return ( - {!forComponentTesting ? : testComponent} + ) } diff --git a/src/stories/WithLoggedInSuperUser.tsx b/src/stories/WithLoggedInSuperUser.tsx index 5141f2eed..8abd55202 100644 --- a/src/stories/WithLoggedInSuperUser.tsx +++ b/src/stories/WithLoggedInSuperUser.tsx @@ -9,7 +9,9 @@ export const WithLoggedInSuperUser = (Story: StoryFn) => { user: UserMother.createSuperUser(), logout: () => Promise.resolve(), setUser: () => {}, - isLoadingUser: false + isLoadingUser: false, + sessionError: null, + refetchUserSession: () => Promise.resolve() }}> diff --git a/src/stories/WithLoggedInUser.tsx b/src/stories/WithLoggedInUser.tsx index 3b3b0eb1e..90224b24e 100644 --- a/src/stories/WithLoggedInUser.tsx +++ b/src/stories/WithLoggedInUser.tsx @@ -9,7 +9,9 @@ export const WithLoggedInUser = (Story: StoryFn) => { user: UserMother.create(), logout: () => Promise.resolve(), setUser: () => {}, - isLoadingUser: false + isLoadingUser: false, + sessionError: null, + refetchUserSession: () => Promise.resolve() }}> diff --git a/tests/component/sections/layout/Layout.spec.tsx b/tests/component/sections/layout/Layout.spec.tsx index 842329ff7..63a18eb65 100644 --- a/tests/component/sections/layout/Layout.spec.tsx +++ b/tests/component/sections/layout/Layout.spec.tsx @@ -18,8 +18,7 @@ describe('Layout', () => { cy.findByText('Dataverse').should('exist') cy.findByRole('button', { name: 'Toggle navigation' }).click() - cy.findByRole('link', { name: 'Sign Up' }).should('exist') - cy.findByRole('link', { name: 'Log In' }).should('exist') + cy.findByRole('button', { name: 'Log In' }).should('exist') }) it('renders the Footer', () => { diff --git a/tests/component/sections/layout/header/Header.spec.tsx b/tests/component/sections/layout/header/Header.spec.tsx index f74b956ef..62ba5a0a4 100644 --- a/tests/component/sections/layout/header/Header.spec.tsx +++ b/tests/component/sections/layout/header/Header.spec.tsx @@ -1,35 +1,18 @@ import { UserMother } from '../../../users/domain/models/UserMother' -import { UserRepository } from '../../../../../src/users/domain/repositories/UserRepository' import { Header } from '../../../../../src/sections/layout/header/Header' -import { SessionProvider } from '../../../../../src/sections/session/SessionProvider' const testUser = UserMother.create() -const userRepository: UserRepository = {} as UserRepository -describe('Header component', () => { - beforeEach(() => { - userRepository.getAuthenticated = cy.stub().resolves(testUser) - userRepository.removeAuthenticated = cy.stub().resolves() - }) +describe('Header component', () => { it('displays the brand', () => { - cy.customMount( - -
- - ) + cy.mountAuthenticated(
) cy.findByRole('link', { name: /Dataverse/ }).should('exist') cy.findByRole('link').should('have.attr', 'href', '/spa/') }) it('displays the user name when the user is logged in', () => { - cy.customMount( - -
- - ) - - cy.wrap(userRepository.getAuthenticated).should('be.calledOnce') + cy.mountAuthenticated(
) cy.findByRole('button', { name: 'Toggle navigation' }).click() cy.findByText(testUser.displayName).should('be.visible') @@ -38,13 +21,7 @@ describe('Header component', () => { }) it('displays the Add Data Button when the user is logged in', () => { - cy.customMount( - -
- - ) - - cy.wrap(userRepository.getAuthenticated).should('be.calledOnce') + cy.mountAuthenticated(
) cy.findByRole('button', { name: 'Toggle navigation' }).click() const addDataBtn = cy.findByRole('button', { name: /Add Data/i }) @@ -54,58 +31,17 @@ describe('Header component', () => { cy.findByRole('link', { name: 'New Dataset' }).should('be.visible') }) - it('displays the Sign Up and Log In links when the user is not logged in', () => { - userRepository.getAuthenticated = cy.stub().resolves() - - cy.customMount( - -
- - ) - - cy.wrap(userRepository.getAuthenticated).should('be.calledOnce') + it('displays the Log In button when the user is not logged in', () => { + cy.customMount(
) cy.findByRole('button', { name: 'Toggle navigation' }).click() - cy.findByRole('link', { name: 'Sign Up' }).should('exist') - cy.contains('Sign Up') - cy.contains('Log In') + cy.findByRole('button', { name: 'Log In' }).should('exist') }) it('does not display the Add Data button when the user is not logged in', () => { - userRepository.getAuthenticated = cy.stub().resolves() - - cy.customMount( - -
- - ) - - cy.wrap(userRepository.getAuthenticated).should('be.calledOnce') + cy.customMount(
) cy.findByRole('button', { name: 'Toggle navigation' }).click() cy.findByRole('button', { name: /Add Data/i }).should('not.exist') }) - - it('log outs the user after clicking Log Out', () => { - cy.customMount( - -
- - ) - - cy.wrap(userRepository.getAuthenticated).should('be.calledOnce') - - cy.findByRole('button', { name: 'Toggle navigation' }).click() - - cy.findByText(testUser.displayName).click() - - cy.findByText('Log Out').click() - - cy.wrap(userRepository.removeAuthenticated).should('be.calledOnce') - - cy.findByText(testUser.displayName).should('not.exist') - - cy.findByText('Log In').should('exist') - cy.findByText('Sign Up').should('exist') - }) }) diff --git a/tests/component/sections/session/useSession.spec.tsx b/tests/component/sections/session/useSession.spec.tsx deleted file mode 100644 index 1d78eaa0e..000000000 --- a/tests/component/sections/session/useSession.spec.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Button } from '@iqss/dataverse-design-system' -import { UserMother } from '../../users/domain/models/UserMother' -import { UserRepository } from '../../../../src/users/domain/repositories/UserRepository' -import { useSession } from '../../../../src/sections/session/SessionContext' -import { SessionProvider } from '../../../../src/sections/session/SessionProvider' - -const testUser = UserMother.create() -const userRepository: UserRepository = {} as UserRepository -describe('useSession', () => { - beforeEach(() => { - userRepository.getAuthenticated = cy.stub().resolves(testUser) - userRepository.removeAuthenticated = cy.stub().resolves() - }) - - it('should set user after fetching from repository', () => { - function TestComponent() { - const { user } = useSession() - - return
{user ? {user.displayName} : <>}
- } - - cy.mount( - - - - ) - - cy.wrap(userRepository.getAuthenticated).should('be.calledOnce') - cy.findByText(testUser.displayName).should('exist') - }) - - it('should unset user after calling logOut on repository', () => { - function TestComponent() { - const { user, logout } = useSession() - const onLogoutClick = () => { - void logout() - } - - return ( -
- {user ? {user.displayName} : <>} - -
- ) - } - - cy.mount( - - - - ) - - cy.wrap(userRepository.getAuthenticated).should('be.calledOnce') - - cy.findByText(testUser.displayName).should('exist') - - cy.findByText('Log Out').click() - - cy.wrap(userRepository.removeAuthenticated).should('be.calledOnce') - - cy.findByText(testUser.displayName).should('not.exist') - }) -}) diff --git a/tests/support/commands.tsx b/tests/support/commands.tsx index 1c23b30ea..95c92b04b 100644 --- a/tests/support/commands.tsx +++ b/tests/support/commands.tsx @@ -43,12 +43,11 @@ import { ThemeProvider } from '@iqss/dataverse-design-system' import { ReactNode } from 'react' import { I18nextProvider } from 'react-i18next' import i18next from '../../src/i18n' -import { UserRepository } from '../../src/users/domain/repositories/UserRepository' import { MemoryRouter } from 'react-router-dom' import { TestsUtils } from '@tests/e2e-integration/shared/TestsUtils' import { Utils } from '@/shared/helpers/Utils' import { OIDC_AUTH_CONFIG } from '@/config' -import { SessionProvider } from '@/sections/session/SessionProvider' +import { SessionContext } from '@/sections/session/SessionContext' // Define your custom mount function @@ -63,24 +62,34 @@ Cypress.Commands.add('customMount', (component: ReactNode) => { }) Cypress.Commands.add('mountAuthenticated', (component: ReactNode) => { - const user = UserMother.create() - const userRepository = {} as UserRepository - userRepository.getAuthenticated = cy.stub().resolves(user) - userRepository.removeAuthenticated = cy.stub().resolves() - return cy.customMount( - + Promise.resolve(), + setUser: () => {}, + isLoadingUser: false, + sessionError: null, + refetchUserSession: () => Promise.resolve() + }}> + {component} + ) }) Cypress.Commands.add('mountSuperuser', (component: ReactNode) => { - const user = UserMother.createSuperUser() - const userRepository = {} as UserRepository - userRepository.getAuthenticated = cy.stub().resolves(user) - userRepository.removeAuthenticated = cy.stub().resolves() - return cy.customMount( - + Promise.resolve(), + setUser: () => {}, + isLoadingUser: false, + sessionError: null, + refetchUserSession: () => Promise.resolve() + }}> + {component} + ) }) From 51532afaaa348b7382a8e2cf31813a5b051ee335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 27 Nov 2024 14:01:15 -0300 Subject: [PATCH 46/97] feat: get terms of use --- public/locales/en/signUp.json | 2 +- src/axiosInstance.ts | 19 +++++--- .../FormFields.tsx | 19 +++++--- src/shared/hooks/useGetTermsOfUse.ts | 44 +++++++++++++++++++ 4 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 src/shared/hooks/useGetTermsOfUse.ts diff --git a/public/locales/en/signUp.json b/public/locales/en/signUp.json index 7eb695f25..346a652bc 100644 --- a/public/locales/en/signUp.json +++ b/public/locales/en/signUp.json @@ -46,7 +46,7 @@ "primaryLabel": "General Terms of Use", "label": "I have read and accept the Dataverse General Terms of Use as outlined above.", "description": "The terms and conditions for using the application and services.", - "required": "You must agree to the terms of use.", + "required": "Please check the box to indicate your acceptance of the General Terms of Use.", "noTerms": "There are no Terms of Use for this Dataverse installation." } }, diff --git a/src/axiosInstance.ts b/src/axiosInstance.ts index b1fd50028..a4a72f175 100644 --- a/src/axiosInstance.ts +++ b/src/axiosInstance.ts @@ -2,6 +2,12 @@ import axios from 'axios' import { OIDC_AUTH_CONFIG, DATAVERSE_BACKEND_URL } from './config' import { Utils } from './shared/helpers/Utils' +declare module 'axios' { + export interface AxiosRequestConfig { + excludeToken?: boolean + } +} + /** * This instance is used to make requests that we do not do through js-dataverse */ @@ -12,13 +18,16 @@ const axiosInstance = axios.create({ }) axiosInstance.interceptors.request.use((config) => { - const token = Utils.getLocalStorageItem( - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) + if (!config.excludeToken) { + const token = Utils.getLocalStorageItem( + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) - if (token) { - config.headers.Authorization = `Bearer ${token}` + if (token) { + config.headers.Authorization = `Bearer ${token}` + } } + return config }) diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx index f8d6facd6..f1a449eac 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx @@ -10,6 +10,8 @@ import { useSession } from '@/sections/session/SessionContext' import { Validator } from '@/shared/helpers/Validator' import { type ValidTokenNotLinkedAccountFormData } from './types' import { ValidTokenNotLinkedAccountFormHelper } from './ValidTokenNotLinkedAccountFormHelper' +import { useGetTermsOfUse } from '@/shared/hooks/useGetTermsOfUse' +import { AppLoader } from '@/sections/shared/layout/app-loader/AppLoader' import styles from './FormFields.module.scss' interface FormFieldsProps { @@ -17,10 +19,13 @@ interface FormFieldsProps { } // TODO:ME - Maybe we should redirect to a welcome page after success? ask if there is one, maybe not the case for this scenario -// TODO:ME - We will need an api call to get the terms of use of the installation -// TODO:ME - Show the registration write error message to the user after encapsulating this call in js-dataverse +// TODO:ME - Ask about the format of the terms of use, html string? just text string? what is shown in the box if there is just a url string ? // TODO:ME - Ask about logout when clicking the Cancel button because of the BEARER_TOKEN_IS_VALID_BUT_NOT_LINKED_MESSAGE error +// TODO:ME - JS-DATAVERSE use case for registration +// TODO:ME - Show the registration write error message to the user after encapsulating this call in js-dataverse +// TODO:ME - JS-DATAVERSE use case for getting the terms of use? how to avoid sending token in this case? + /* This is the expected response from the server after succesfull registration, will help for js-dataverse-client-javascript const resp = { @@ -42,7 +47,7 @@ export const FormFields = ({ formDefaultValues }: FormFieldsProps) => { const { t } = useTranslation('signUp') const { t: tShared } = useTranslation('shared') - const hasTermsOfUse = false + const { termsOfUse, isLoading: isLoadingTermsOfUse } = useGetTermsOfUse() const isUsernameRequired = formDefaultValues.username === '' const isEmailRequired = formDefaultValues.emailAddress === '' @@ -116,6 +121,10 @@ export const FormFields = ({ formDefaultValues }: FormFieldsProps) => { const hasAcceptedTheTermsOfUse = form.watch('termsAccepted') + if (isLoadingTermsOfUse) { + return + } + return (
{/*
@@ -293,9 +302,9 @@ export const FormFields = ({ formDefaultValues }: FormFieldsProps) => { { + const [termsOfUse, setTermsOfUse] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + const handleGetUseOfTerms = async () => { + setIsLoading(true) + try { + const response = await axiosInstance.get<{ data: { message: string } }>( + '/api/v1/info/apiTermsOfUse', + { excludeToken: true } + ) + + setTermsOfUse(response.data.data.message) + } catch (err) { + const errorMessage = + err instanceof Error && err.message + ? err.message + : 'Something went wrong getting the use of terms. Try again later.' + setError(errorMessage) + } finally { + setIsLoading(false) + } + } + + void handleGetUseOfTerms() + }, []) + + return { + termsOfUse, + error, + isLoading + } +} From 42e800f90b820c6848f318cc7c557906e767373a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 27 Nov 2024 14:07:09 -0300 Subject: [PATCH 47/97] test: fix request access modal tests --- .../access-file-menu/RequestAccessModal.tsx | 2 +- .../RequestAccessModal.spec.tsx | 20 ++++--------------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/sections/file/file-action-buttons/access-file-menu/RequestAccessModal.tsx b/src/sections/file/file-action-buttons/access-file-menu/RequestAccessModal.tsx index 7e44b4ad3..d4a7b49a4 100644 --- a/src/sections/file/file-action-buttons/access-file-menu/RequestAccessModal.tsx +++ b/src/sections/file/file-action-buttons/access-file-menu/RequestAccessModal.tsx @@ -94,7 +94,7 @@ const RequestAccessLoginMessage = ({ handleClose }: { handleClose: () => void }) variant="link" onClick={() => oidcLogin(encodeReturnToPathInStateQueryParam(`${pathname}${search}`))} className="p-0 align-baseline"> - log in + Log In {' '} to request access.

diff --git a/tests/component/sections/file/file-action-buttons/access-file-menu/RequestAccessModal.spec.tsx b/tests/component/sections/file/file-action-buttons/access-file-menu/RequestAccessModal.spec.tsx index 74325fce7..bc4878205 100644 --- a/tests/component/sections/file/file-action-buttons/access-file-menu/RequestAccessModal.spec.tsx +++ b/tests/component/sections/file/file-action-buttons/access-file-menu/RequestAccessModal.spec.tsx @@ -1,7 +1,4 @@ import { RequestAccessModal } from '../../../../../../src/sections/file/file-action-buttons/access-file-menu/RequestAccessModal' -import { UserMother } from '../../../../users/domain/models/UserMother' -import { UserRepository } from '../../../../../../src/users/domain/repositories/UserRepository' -import { SessionProvider } from '../../../../../../src/sections/session/SessionProvider' import { Route } from '../../../../../../src/sections/Route.enum' import { FilePreviewMother } from '../../../../files/domain/models/FilePreviewMother' @@ -22,9 +19,8 @@ describe('RequestAccessModal', () => { cy.findByRole('dialog').should('exist') cy.findAllByText('Request Access').should('exist') - cy.findByRole('link', { name: 'Log In' }) - .should('exist') - .should('have.attr', 'href', Route.LOG_IN_JSF) + cy.findByRole('button', { name: 'Log In' }).should('exist') + cy.findByRole('link', { name: 'Sign Up' }) .should('exist') .should('have.attr', 'href', Route.SIGN_UP_JSF) @@ -35,16 +31,8 @@ describe('RequestAccessModal', () => { it('shows request access modal when button is clicked and user is logged in', () => { const file = FilePreviewMother.create() - const user = UserMother.create() - const userRepository = {} as UserRepository - userRepository.getAuthenticated = cy.stub().resolves(user) - userRepository.removeAuthenticated = cy.stub().resolves() - - cy.customMount( - - - - ) + + cy.mountAuthenticated() cy.findByRole('button', { name: 'Request Access' }).click() From e674e260d1ba2920e1045e42de5f6fa59e4093fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 28 Nov 2024 11:26:55 -0300 Subject: [PATCH 48/97] feat: implement story --- src/stories/WithOIDCAuthContext.tsx | 22 +++++++++++ .../DataverseInfoMockLoadingkRepository.ts | 13 +++++++ .../info/DataverseInfoMockRepository.ts | 23 +++++++++++ src/stories/sign-up/SignUp.stories.tsx | 39 +++++++++++++++++++ tests/component/auth/AuthContextMother.ts | 37 ++++++++++++++++++ .../component/info/models/TermsOfUseMother.ts | 13 +++++++ 6 files changed, 147 insertions(+) create mode 100644 src/stories/WithOIDCAuthContext.tsx create mode 100644 src/stories/shared-mock-repositories/info/DataverseInfoMockLoadingkRepository.ts create mode 100644 src/stories/shared-mock-repositories/info/DataverseInfoMockRepository.ts create mode 100644 src/stories/sign-up/SignUp.stories.tsx create mode 100644 tests/component/auth/AuthContextMother.ts create mode 100644 tests/component/info/models/TermsOfUseMother.ts diff --git a/src/stories/WithOIDCAuthContext.tsx b/src/stories/WithOIDCAuthContext.tsx new file mode 100644 index 000000000..aadf04e49 --- /dev/null +++ b/src/stories/WithOIDCAuthContext.tsx @@ -0,0 +1,22 @@ +import { StoryFn } from '@storybook/react' +import { AuthContext } from 'react-oauth2-code-pkce' +import { AuthContextMother } from '@tests/component/auth/AuthContextMother' + +export const WithOIDCAuthContext = (Story: StoryFn) => { + return ( + {}, + logOut: () => {}, + loginInProgress: false, + tokenData: AuthContextMother.createTokenData(), + idTokenData: AuthContextMother.createTokenData(), + error: null, + login: () => {} // 👈 deprecated + }}> + + + ) +} diff --git a/src/stories/shared-mock-repositories/info/DataverseInfoMockLoadingkRepository.ts b/src/stories/shared-mock-repositories/info/DataverseInfoMockLoadingkRepository.ts new file mode 100644 index 000000000..b6916f67a --- /dev/null +++ b/src/stories/shared-mock-repositories/info/DataverseInfoMockLoadingkRepository.ts @@ -0,0 +1,13 @@ +import { DataverseInfoMockRepository } from './DataverseInfoMockRepository' +import { DataverseVersion } from '@/info/domain/models/DataverseVersion' +import { TermsOfUse } from '@/info/domain/models/TermsOfUse' + +export class DataverseInfoMockLoadingRepository implements DataverseInfoMockRepository { + getVersion(): Promise { + return new Promise(() => {}) + } + + getTermsOfUse(): Promise { + return new Promise(() => {}) + } +} diff --git a/src/stories/shared-mock-repositories/info/DataverseInfoMockRepository.ts b/src/stories/shared-mock-repositories/info/DataverseInfoMockRepository.ts new file mode 100644 index 000000000..7e0671c69 --- /dev/null +++ b/src/stories/shared-mock-repositories/info/DataverseInfoMockRepository.ts @@ -0,0 +1,23 @@ +import { DataverseVersion } from '@/info/domain/models/DataverseVersion' +import { TermsOfUse } from '@/info/domain/models/TermsOfUse' +import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' +import { DataverseVersionMother } from '@tests/component/info/models/DataverseVersionMother' +import { TermsOfUseMother } from '@tests/component/info/models/TermsOfUseMother' + +export class DataverseInfoMockRepository implements DataverseInfoRepository { + getVersion(): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(DataverseVersionMother.create()) + }, 1_000) + }) + } + + getTermsOfUse(): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(TermsOfUseMother.create()) + }, 1_000) + }) + } +} diff --git a/src/stories/sign-up/SignUp.stories.tsx b/src/stories/sign-up/SignUp.stories.tsx new file mode 100644 index 000000000..6eab0658d --- /dev/null +++ b/src/stories/sign-up/SignUp.stories.tsx @@ -0,0 +1,39 @@ +import type { StoryObj, Meta } from '@storybook/react' +import { WithLayout } from '../WithLayout' +import { WithI18next } from '../WithI18next' +import { SignUp } from '@/sections/sign-up/SignUp' +import { DataverseInfoMockRepository } from '../shared-mock-repositories/info/DataverseInfoMockRepository' +import { DataverseInfoMockLoadingRepository } from '../shared-mock-repositories/info/DataverseInfoMocLoadingkRepository' +import { WithOIDCAuthContext } from '../WithOIDCAuthContext' + +// TODO:ME - After implementing register use case in js-dataverse, we should mock the register function here also. + +const meta: Meta = { + title: 'Pages/Sign Up', + component: SignUp, + decorators: [WithI18next, WithLayout, WithOIDCAuthContext], + parameters: { + // Sets the delay for all stories. + chromatic: { delay: 15000, pauseAnimationAtEnd: true } + } +} +export default meta +type Story = StoryObj + +export const ValidTokenWithNotLinkedAccount: Story = { + render: () => ( + + ) +} + +export const Loading: Story = { + render: () => ( + + ) +} diff --git a/tests/component/auth/AuthContextMother.ts b/tests/component/auth/AuthContextMother.ts new file mode 100644 index 000000000..f5e231176 --- /dev/null +++ b/tests/component/auth/AuthContextMother.ts @@ -0,0 +1,37 @@ +export class AuthContextMother { + static createToken() { + return 'some.fake.token' + } + + static createTokenData() { + return { + exp: 1732803352, + iat: 1732803052, + auth_time: 1732799407, + jti: 'some-fake-jti-number', + iss: 'http://localhost:8000/realms/test', + aud: 'account', + sub: 'some-fake-sub-number', + typ: 'Bearer', + azp: 'test', + session_state: 'some-fake-session-state-number', + acr: '0', + realm_access: { + roles: ['default-roles-test', 'offline_access', 'uma_authorization'] + }, + resource_access: { + account: { + roles: ['manage-account', 'manage-account-links', 'view-profile'] + } + }, + scope: 'openid profile email', + sid: 'some-fake-sid-number', + email_verified: true, + name: 'Dataverse User', + preferred_username: 'user', + given_name: 'Dataverse', + family_name: 'User', + email: 'dataverse-user@mailinator.com' + } + } +} diff --git a/tests/component/info/models/TermsOfUseMother.ts b/tests/component/info/models/TermsOfUseMother.ts new file mode 100644 index 000000000..64933502d --- /dev/null +++ b/tests/component/info/models/TermsOfUseMother.ts @@ -0,0 +1,13 @@ +import { faker } from '@faker-js/faker' +import isChromatic from 'chromatic/isChromatic' +import { TermsOfUse } from '@/info/domain/models/TermsOfUse' + +export class TermsOfUseMother { + static create(): TermsOfUse { + return isChromatic() ? 'https://some-terms-of-use-url.com' : faker.lorem.paragraphs(8) + } + + static createEmpty(): TermsOfUse { + return 'There are no API Terms of Use for this Dataverse installation.' + } +} From edf442950fb2ff31e73b36003bce26266f84ba0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 28 Nov 2024 11:36:13 -0300 Subject: [PATCH 49/97] feat: get terms of use repository convention --- src/info/domain/models/TermsOfUse.ts | 1 + .../repositories/DataverseInfoRepository.ts | 2 + src/info/domain/useCases/getTermsOfUse.ts | 10 +++ .../DataverseInfoJSDataverseRepository.ts | 15 ++++- src/sections/sign-up/SignUp.tsx | 11 +++- src/sections/sign-up/SignUpFactory.tsx | 10 ++- .../FormFields.tsx | 20 +++--- .../FormFieldsSkeleton.tsx | 46 ++++++++++++++ .../ValidTokenNotLinkedAccountForm.tsx | 20 +++++- src/shared/hooks/useGetTermsOfUse.ts | 15 +++-- src/stories/sign-up/SignUp.stories.tsx | 2 +- .../shared/hooks/useGetTermsOfUse.spec.ts | 62 +++++++++++++++++++ 12 files changed, 184 insertions(+), 30 deletions(-) create mode 100644 src/info/domain/models/TermsOfUse.ts create mode 100644 src/info/domain/useCases/getTermsOfUse.ts create mode 100644 src/sections/sign-up/valid-token-not-linked-account-form/FormFieldsSkeleton.tsx create mode 100644 tests/component/shared/hooks/useGetTermsOfUse.spec.ts diff --git a/src/info/domain/models/TermsOfUse.ts b/src/info/domain/models/TermsOfUse.ts new file mode 100644 index 000000000..c69bad018 --- /dev/null +++ b/src/info/domain/models/TermsOfUse.ts @@ -0,0 +1 @@ +export type TermsOfUse = string | null diff --git a/src/info/domain/repositories/DataverseInfoRepository.ts b/src/info/domain/repositories/DataverseInfoRepository.ts index e984f030d..9c8cd4f3d 100644 --- a/src/info/domain/repositories/DataverseInfoRepository.ts +++ b/src/info/domain/repositories/DataverseInfoRepository.ts @@ -1,5 +1,7 @@ import { DataverseVersion } from '../models/DataverseVersion' +import { TermsOfUse } from '../models/TermsOfUse' export interface DataverseInfoRepository { getVersion(): Promise + getTermsOfUse: () => Promise } diff --git a/src/info/domain/useCases/getTermsOfUse.ts b/src/info/domain/useCases/getTermsOfUse.ts new file mode 100644 index 000000000..53eaa2324 --- /dev/null +++ b/src/info/domain/useCases/getTermsOfUse.ts @@ -0,0 +1,10 @@ +import { type TermsOfUse } from '../../../info/domain/models/TermsOfUse' +import { DataverseInfoRepository } from '../repositories/DataverseInfoRepository' + +export function getTermsOfUse( + dataverseInfoRepository: DataverseInfoRepository +): Promise { + return dataverseInfoRepository.getTermsOfUse().catch((error) => { + throw error + }) +} diff --git a/src/info/infrastructure/repositories/DataverseInfoJSDataverseRepository.ts b/src/info/infrastructure/repositories/DataverseInfoJSDataverseRepository.ts index f02dde979..a7049c46a 100644 --- a/src/info/infrastructure/repositories/DataverseInfoJSDataverseRepository.ts +++ b/src/info/infrastructure/repositories/DataverseInfoJSDataverseRepository.ts @@ -1,6 +1,8 @@ import { getDataverseVersion, ReadError } from '@iqss/dataverse-client-javascript' -import { DataverseInfoRepository } from '../../domain/repositories/DataverseInfoRepository' -import { DataverseVersion } from '../../domain/models/DataverseVersion' +import { axiosInstance } from '@/axiosInstance' +import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' +import { DataverseVersion } from '@/info/domain/models/DataverseVersion' +import { TermsOfUse } from '@/info/domain/models/TermsOfUse' interface JSDataverseDataverseVersion { number: string @@ -26,4 +28,13 @@ export class DataverseInfoJSDataverseRepository implements DataverseInfoReposito throw new Error(error.message) }) } + + async getTermsOfUse() { + //TODO - implement using js-dataverse + const response = await axiosInstance.get<{ data: { message: TermsOfUse } }>( + '/api/v1/info/apiTermsOfUse', + { excludeToken: true } + ) + return response.data.data.message + } } diff --git a/src/sections/sign-up/SignUp.tsx b/src/sections/sign-up/SignUp.tsx index 96cefcc1f..8a48e8eac 100644 --- a/src/sections/sign-up/SignUp.tsx +++ b/src/sections/sign-up/SignUp.tsx @@ -1,6 +1,7 @@ import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Alert, Tabs } from '@iqss/dataverse-design-system' +import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' import { useLoading } from '../loading/LoadingContext' import { ValidTokenNotLinkedAccountForm } from './valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm' import styles from './SignUp.module.scss' @@ -9,10 +10,14 @@ import styles from './SignUp.module.scss' // TODO:ME - How to handle 401 Unauthorized {"status":"ERROR","message":"Unauthorized bearer token."} globally, maybe redirect to oidc login page? interface SignUpProps { + dataverseInfoRepository: DataverseInfoRepository hasValidTokenButNotLinkedAccount: boolean } -export const SignUp = ({ hasValidTokenButNotLinkedAccount }: SignUpProps) => { +export const SignUp = ({ + dataverseInfoRepository, + hasValidTokenButNotLinkedAccount +}: SignUpProps) => { const { t } = useTranslation('signUp') const { setIsLoading } = useLoading() @@ -46,7 +51,9 @@ export const SignUp = ({ hasValidTokenButNotLinkedAccount }: SignUpProps) => {
- + {hasValidTokenButNotLinkedAccount && ( + + )}
diff --git a/src/sections/sign-up/SignUpFactory.tsx b/src/sections/sign-up/SignUpFactory.tsx index 1644903b2..3444d7fba 100644 --- a/src/sections/sign-up/SignUpFactory.tsx +++ b/src/sections/sign-up/SignUpFactory.tsx @@ -2,6 +2,9 @@ import { ReactElement } from 'react' import { useSearchParams } from 'react-router-dom' import { SignUp } from './SignUp' import { QueryParamKey } from '../Route.enum' +import { DataverseInfoJSDataverseRepository } from '@/info/infrastructure/repositories/DataverseInfoJSDataverseRepository' + +const dataverseInfoRepository = new DataverseInfoJSDataverseRepository() export class SignUpFactory { static create(): ReactElement { @@ -15,5 +18,10 @@ function SignUpWithSearchParams() { const hasValidTokenButNotLinkedAccount = searchParams.get(QueryParamKey.VALID_TOKEN_BUT_NOT_LINKED_ACCOUNT) === 'true' - return + return ( + + ) } diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx index f1a449eac..12fdb2712 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx @@ -10,14 +10,9 @@ import { useSession } from '@/sections/session/SessionContext' import { Validator } from '@/shared/helpers/Validator' import { type ValidTokenNotLinkedAccountFormData } from './types' import { ValidTokenNotLinkedAccountFormHelper } from './ValidTokenNotLinkedAccountFormHelper' -import { useGetTermsOfUse } from '@/shared/hooks/useGetTermsOfUse' -import { AppLoader } from '@/sections/shared/layout/app-loader/AppLoader' +import { TermsOfUse } from '@/info/domain/models/TermsOfUse' import styles from './FormFields.module.scss' -interface FormFieldsProps { - formDefaultValues: ValidTokenNotLinkedAccountFormData -} - // TODO:ME - Maybe we should redirect to a welcome page after success? ask if there is one, maybe not the case for this scenario // TODO:ME - Ask about the format of the terms of use, html string? just text string? what is shown in the box if there is just a url string ? // TODO:ME - Ask about logout when clicking the Cancel button because of the BEARER_TOKEN_IS_VALID_BUT_NOT_LINKED_MESSAGE error @@ -40,15 +35,18 @@ interface FormFieldsProps { } */ -export const FormFields = ({ formDefaultValues }: FormFieldsProps) => { +interface FormFieldsProps { + formDefaultValues: ValidTokenNotLinkedAccountFormData + termsOfUse: TermsOfUse +} + +export const FormFields = ({ formDefaultValues, termsOfUse }: FormFieldsProps) => { const navigate = useNavigate() const { refetchUserSession } = useSession() const { tokenData, logOut: oidcLogout } = useContext(AuthContext) const { t } = useTranslation('signUp') const { t: tShared } = useTranslation('shared') - const { termsOfUse, isLoading: isLoadingTermsOfUse } = useGetTermsOfUse() - const isUsernameRequired = formDefaultValues.username === '' const isEmailRequired = formDefaultValues.emailAddress === '' const isFirstNameRequired = formDefaultValues.firstName === '' @@ -121,10 +119,6 @@ export const FormFields = ({ formDefaultValues }: FormFieldsProps) => { const hasAcceptedTheTermsOfUse = form.watch('termsAccepted') - if (isLoadingTermsOfUse) { - return - } - return (
{/*
diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFieldsSkeleton.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/FormFieldsSkeleton.tsx new file mode 100644 index 000000000..fd1b3b021 --- /dev/null +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFieldsSkeleton.tsx @@ -0,0 +1,46 @@ +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton' +import { Col, Row, Stack } from '@iqss/dataverse-design-system' + +export const FormFieldsSkeleton = () => ( + +
+ + + + + + + + + + + + +
+
+) + +interface LabelAndFieldSkeletonProps { + withHelperText?: boolean + termsOfUse?: boolean +} + +const LabelAndFieldSkeleton = ({ withHelperText, termsOfUse }: LabelAndFieldSkeletonProps) => ( + + + + + + + {withHelperText && } + + {termsOfUse && ( + + + + + )} + + + +) diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx index cf7aa7b2e..103ede6a4 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx @@ -1,11 +1,21 @@ import { useContext } from 'react' import { AuthContext } from 'react-oauth2-code-pkce' +import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' +import { useGetTermsOfUse } from '@/shared/hooks/useGetTermsOfUse' import { OIDC_STANDARD_CLAIMS, type ValidTokenNotLinkedAccountFormData } from './types' -import { FormFields } from './FormFields' import { ValidTokenNotLinkedAccountFormHelper } from './ValidTokenNotLinkedAccountFormHelper' +import { FormFields } from './FormFields' +import { FormFieldsSkeleton } from './FormFieldsSkeleton' + +interface ValidTokenNotLinkedAccountFormProps { + dataverseInfoRepository: DataverseInfoRepository +} -export const ValidTokenNotLinkedAccountForm = () => { +export const ValidTokenNotLinkedAccountForm = ({ + dataverseInfoRepository +}: ValidTokenNotLinkedAccountFormProps) => { const { tokenData } = useContext(AuthContext) + const { termsOfUse, isLoading: isLoadingTermsOfUse } = useGetTermsOfUse(dataverseInfoRepository) const defaultUserName = ValidTokenNotLinkedAccountFormHelper.getTokenDataValue( @@ -45,5 +55,9 @@ export const ValidTokenNotLinkedAccountForm = () => { termsAccepted: false } - return + if (isLoadingTermsOfUse) { + return + } + + return } diff --git a/src/shared/hooks/useGetTermsOfUse.ts b/src/shared/hooks/useGetTermsOfUse.ts index 55a5ebb5e..a12908ec8 100644 --- a/src/shared/hooks/useGetTermsOfUse.ts +++ b/src/shared/hooks/useGetTermsOfUse.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { axiosInstance } from '@/axiosInstance' +import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' interface UseGetTermsOfUseReturnType { termsOfUse: string | null @@ -7,7 +7,9 @@ interface UseGetTermsOfUseReturnType { isLoading: boolean } -export const useGetTermsOfUse = (): UseGetTermsOfUseReturnType => { +export const useGetTermsOfUse = ( + dataverseInfoRepository: DataverseInfoRepository +): UseGetTermsOfUseReturnType => { const [termsOfUse, setTermsOfUse] = useState(null) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) @@ -16,12 +18,9 @@ export const useGetTermsOfUse = (): UseGetTermsOfUseReturnType => { const handleGetUseOfTerms = async () => { setIsLoading(true) try { - const response = await axiosInstance.get<{ data: { message: string } }>( - '/api/v1/info/apiTermsOfUse', - { excludeToken: true } - ) + const termsOfUse = await dataverseInfoRepository.getTermsOfUse() - setTermsOfUse(response.data.data.message) + setTermsOfUse(termsOfUse) } catch (err) { const errorMessage = err instanceof Error && err.message @@ -34,7 +33,7 @@ export const useGetTermsOfUse = (): UseGetTermsOfUseReturnType => { } void handleGetUseOfTerms() - }, []) + }, [dataverseInfoRepository]) return { termsOfUse, diff --git a/src/stories/sign-up/SignUp.stories.tsx b/src/stories/sign-up/SignUp.stories.tsx index 6eab0658d..82773b97e 100644 --- a/src/stories/sign-up/SignUp.stories.tsx +++ b/src/stories/sign-up/SignUp.stories.tsx @@ -3,7 +3,7 @@ import { WithLayout } from '../WithLayout' import { WithI18next } from '../WithI18next' import { SignUp } from '@/sections/sign-up/SignUp' import { DataverseInfoMockRepository } from '../shared-mock-repositories/info/DataverseInfoMockRepository' -import { DataverseInfoMockLoadingRepository } from '../shared-mock-repositories/info/DataverseInfoMocLoadingkRepository' +import { DataverseInfoMockLoadingRepository } from '../shared-mock-repositories/info/DataverseInfoMockLoadingkRepository' import { WithOIDCAuthContext } from '../WithOIDCAuthContext' // TODO:ME - After implementing register use case in js-dataverse, we should mock the register function here also. diff --git a/tests/component/shared/hooks/useGetTermsOfUse.spec.ts b/tests/component/shared/hooks/useGetTermsOfUse.spec.ts new file mode 100644 index 000000000..3ba2188c2 --- /dev/null +++ b/tests/component/shared/hooks/useGetTermsOfUse.spec.ts @@ -0,0 +1,62 @@ +import { act, renderHook } from '@testing-library/react' +import { useGetTermsOfUse } from '@/shared/hooks/useGetTermsOfUse' +import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' +import { TermsOfUseMother } from '@tests/component/info/models/TermsOfUseMother' + +const dataverseInfoRepository: DataverseInfoRepository = {} as DataverseInfoRepository +const termsOfUseMock = TermsOfUseMother.create() + +describe('useGetTermsOfUse', () => { + it('should return terms of use correctly', async () => { + dataverseInfoRepository.getTermsOfUse = cy.stub().resolves(termsOfUseMock) + + const { result } = renderHook(() => useGetTermsOfUse(dataverseInfoRepository)) + + await act(() => { + expect(result.current.isLoading).to.deep.equal(true) + return expect(result.current.termsOfUse).to.deep.equal(null) + }) + + await act(() => { + expect(result.current.isLoading).to.deep.equal(false) + + return expect(result.current.termsOfUse).to.deep.equal(termsOfUseMock) + }) + }) + + describe('Error handling', () => { + it('should return correct error message when there is an error type catched', async () => { + dataverseInfoRepository.getTermsOfUse = cy.stub().rejects(new Error('Error message')) + + const { result } = renderHook(() => useGetTermsOfUse(dataverseInfoRepository)) + + await act(() => { + expect(result.current.isLoading).to.deep.equal(true) + return expect(result.current.error).to.deep.equal(null) + }) + + await act(() => { + expect(result.current.isLoading).to.deep.equal(false) + return expect(result.current.error).to.deep.equal('Error message') + }) + }) + + it('should return correct error message when there is not an error type catched', async () => { + dataverseInfoRepository.getTermsOfUse = cy.stub().rejects('Error message') + + const { result } = renderHook(() => useGetTermsOfUse(dataverseInfoRepository)) + + await act(() => { + expect(result.current.isLoading).to.deep.equal(true) + return expect(result.current.error).to.deep.equal(null) + }) + + await act(() => { + expect(result.current.isLoading).to.deep.equal(false) + return expect(result.current.error).to.deep.equal( + 'Something went wrong getting the use of terms. Try again later.' + ) + }) + }) + }) +}) From f3e46893732eb5ff84b52774c96ab55dc9a6a9d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 28 Nov 2024 11:53:24 -0300 Subject: [PATCH 50/97] feat: add aria-required attrs --- .../valid-token-not-linked-account-form/FormFields.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx index 12fdb2712..9f34a9234 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx @@ -16,7 +16,6 @@ import styles from './FormFields.module.scss' // TODO:ME - Maybe we should redirect to a welcome page after success? ask if there is one, maybe not the case for this scenario // TODO:ME - Ask about the format of the terms of use, html string? just text string? what is shown in the box if there is just a url string ? // TODO:ME - Ask about logout when clicking the Cancel button because of the BEARER_TOKEN_IS_VALID_BUT_NOT_LINKED_MESSAGE error - // TODO:ME - JS-DATAVERSE use case for registration // TODO:ME - Show the registration write error message to the user after encapsulating this call in js-dataverse // TODO:ME - JS-DATAVERSE use case for getting the terms of use? how to avoid sending token in this case? @@ -153,6 +152,7 @@ export const FormFields = ({ formDefaultValues, termsOfUse }: FormFieldsProps) = isInvalid={invalid} ref={ref} disabled={!isUsernameRequired} + aria-required={isUsernameRequired} /> {error?.message}
@@ -180,6 +180,7 @@ export const FormFields = ({ formDefaultValues, termsOfUse }: FormFieldsProps) = isInvalid={invalid} ref={ref} disabled={!isFirstNameRequired} + aria-required={isFirstNameRequired} /> {error?.message} @@ -205,6 +206,7 @@ export const FormFields = ({ formDefaultValues, termsOfUse }: FormFieldsProps) = isInvalid={invalid} ref={ref} disabled={!isLastNameRequired} + aria-required={isLastNameRequired} /> {error?.message} @@ -230,6 +232,7 @@ export const FormFields = ({ formDefaultValues, termsOfUse }: FormFieldsProps) = isInvalid={invalid} ref={ref} disabled={!isEmailRequired} + aria-required={isEmailRequired} /> {error?.message} From dfcf7ac05720a679d4ee9b5635ace033c234b289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 28 Nov 2024 14:29:54 -0300 Subject: [PATCH 51/97] feat(design system): extend Table props --- packages/design-system/CHANGELOG.md | 2 +- .../src/lib/components/table/Table.tsx | 11 ++++++-- .../src/lib/stories/table/Table.stories.tsx | 25 +++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/design-system/CHANGELOG.md b/packages/design-system/CHANGELOG.md index 897311d36..cd6b6b237 100644 --- a/packages/design-system/CHANGELOG.md +++ b/packages/design-system/CHANGELOG.md @@ -47,7 +47,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline - **FormSelect:** extend Props Interface to accept `autoFocus` prop. - **Stack:** NEW Stack element to manage layouts. - **TransferList:** NEW TransferList component to transfer items between two list, also sortable. -- **Table:** extend Props Interface to accept `bordered` prop to add or remove borders on all sides of the table and cells. Defaults to true. +- **Table:** extend Props Interface to accept `bordered`, `borderless` and `striped`. - **Spinner:** New Spinner component. - **CloseButton:** NEW close button component. - **Tab:** extend Props Interface to accept `disabled` prop to disable the tab. diff --git a/packages/design-system/src/lib/components/table/Table.tsx b/packages/design-system/src/lib/components/table/Table.tsx index 1548a467a..9ee2928fc 100644 --- a/packages/design-system/src/lib/components/table/Table.tsx +++ b/packages/design-system/src/lib/components/table/Table.tsx @@ -2,13 +2,20 @@ import { Table as TableBS } from 'react-bootstrap' import styles from './Table.module.scss' interface TableProps { + striped?: boolean bordered?: boolean + borderless?: boolean children: React.ReactNode } -export function Table({ bordered = true, children }: TableProps) { +export function Table({ + striped = true, + bordered = true, + borderless = false, + children +}: TableProps) { return ( - + {children} ) diff --git a/packages/design-system/src/lib/stories/table/Table.stories.tsx b/packages/design-system/src/lib/stories/table/Table.stories.tsx index db2b6c0c6..32f28daaf 100644 --- a/packages/design-system/src/lib/stories/table/Table.stories.tsx +++ b/packages/design-system/src/lib/stories/table/Table.stories.tsx @@ -60,3 +60,28 @@ export const Default: Story = { ) } + +export const WithoutBordersAndStrips: Story = { + render: () => ( + + + + + + + + + + + + + + + + + + + +
Usernamejohndoe
Given NameJohn
Family NameDoe
Emailjohndoe@email.com
+ ) +} From d6c8c26b1c94e3574791e3bb259cfd0854dc5bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 28 Nov 2024 14:30:52 -0300 Subject: [PATCH 52/97] feat: extend User interface with identifier --- src/users/domain/models/User.ts | 1 + .../infrastructure/mappers/JSUserMapper.ts | 21 +++++++++++++++++++ .../repositories/UserJSDataverseRepository.ts | 11 ++-------- .../users/domain/models/UserMother.ts | 6 ++++-- 4 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 src/users/infrastructure/mappers/JSUserMapper.ts diff --git a/src/users/domain/models/User.ts b/src/users/domain/models/User.ts index 098799ead..30ccf24c7 100644 --- a/src/users/domain/models/User.ts +++ b/src/users/domain/models/User.ts @@ -5,5 +5,6 @@ export interface User { firstName: string lastName: string email: string + identifier: string affiliation?: string } diff --git a/src/users/infrastructure/mappers/JSUserMapper.ts b/src/users/infrastructure/mappers/JSUserMapper.ts new file mode 100644 index 000000000..a69850d8e --- /dev/null +++ b/src/users/infrastructure/mappers/JSUserMapper.ts @@ -0,0 +1,21 @@ +import { User } from '@/users/domain/models/User' +import { AuthenticatedUser } from '@iqss/dataverse-client-javascript' + +export class JSUserMapper { + static toUser(authenticatedUser: AuthenticatedUser): User { + return { + displayName: authenticatedUser.displayName, + persistentId: authenticatedUser.persistentUserId, + firstName: authenticatedUser.firstName, + lastName: authenticatedUser.lastName, + email: authenticatedUser.email, + affiliation: authenticatedUser.affiliation, + superuser: authenticatedUser.superuser, + identifier: this.removeAtSymbol(authenticatedUser.identifier) + } + } + + static removeAtSymbol(identifier: string): string { + return identifier.startsWith('@') ? identifier.slice(1) : identifier + } +} diff --git a/src/users/infrastructure/repositories/UserJSDataverseRepository.ts b/src/users/infrastructure/repositories/UserJSDataverseRepository.ts index bb34659c4..d3409dffa 100644 --- a/src/users/infrastructure/repositories/UserJSDataverseRepository.ts +++ b/src/users/infrastructure/repositories/UserJSDataverseRepository.ts @@ -9,6 +9,7 @@ import { deleteCurrentApiToken } from '@iqss/dataverse-client-javascript/dist/users' import { logout, ReadError, WriteError } from '@iqss/dataverse-client-javascript' +import { JSUserMapper } from '../mappers/JSUserMapper' interface ApiTokenInfoPayload { apiToken: string @@ -20,15 +21,7 @@ export class UserJSDataverseRepository implements UserRepository { return getCurrentAuthenticatedUser .execute() .then((authenticatedUser: AuthenticatedUser) => { - return { - displayName: authenticatedUser.displayName, - persistentId: authenticatedUser.persistentUserId, - firstName: authenticatedUser.firstName, - lastName: authenticatedUser.lastName, - email: authenticatedUser.email, - affiliation: authenticatedUser.affiliation, - superuser: authenticatedUser.superuser - } + return JSUserMapper.toUser(authenticatedUser) }) .catch((error: ReadError) => { throw error diff --git a/tests/component/users/domain/models/UserMother.ts b/tests/component/users/domain/models/UserMother.ts index 6b3576784..92a06fb95 100644 --- a/tests/component/users/domain/models/UserMother.ts +++ b/tests/component/users/domain/models/UserMother.ts @@ -9,7 +9,8 @@ export class UserMother { lastName: 'Potts', email: 'jamesPotts@g.harvard.edu', affiliation: 'Harvard University', - superuser: false + superuser: false, + identifier: 'jamespotts' } } static createSuperUser(): User { @@ -20,7 +21,8 @@ export class UserMother { firstName: 'James', lastName: 'Potts', email: 'jamesPotts@g.harvard.edu', - affiliation: 'Harvard University' + affiliation: 'Harvard University', + identifier: 'jamespotts' } } } From 884dfa6b297590aec9d27b6057e5bd9c7f6ce7ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 28 Nov 2024 14:49:46 -0300 Subject: [PATCH 53/97] feat: redirect user to account info tab after registration --- public/locales/en/account.json | 6 ++++ public/locales/en/header.json | 1 + src/sections/account/Account.tsx | 10 +++--- .../AccountInfoSection.tsx | 35 +++++++++++++++++++ .../layout/header/LoggedInHeaderActions.tsx | 5 +++ .../FormFields.tsx | 7 +++- 6 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 src/sections/account/account-info-section/AccountInfoSection.tsx diff --git a/public/locales/en/account.json b/public/locales/en/account.json index 41b74e884..23388c11b 100644 --- a/public/locales/en/account.json +++ b/public/locales/en/account.json @@ -14,5 +14,11 @@ "recreateToken": "Recreate Token", "revokeToken": "Revoke Token", "createToken": "Create Token" + }, + "info": { + "username": "Username", + "givenName": "Given Name", + "familyName": "Family Name", + "email": "Email" } } diff --git a/public/locales/en/header.json b/public/locales/en/header.json index b9313292b..6d55e46dd 100644 --- a/public/locales/en/header.json +++ b/public/locales/en/header.json @@ -8,6 +8,7 @@ "addData": "Add Data", "newCollection": "New Collection", "newDataset": "New Dataset", + "accountInfo": "Account Information", "apiToken": "API Token" } } diff --git a/src/sections/account/Account.tsx b/src/sections/account/Account.tsx index a9e4a77a8..1c74c7028 100644 --- a/src/sections/account/Account.tsx +++ b/src/sections/account/Account.tsx @@ -3,6 +3,7 @@ import { Tabs } from '@iqss/dataverse-design-system' import { AccountHelper, AccountPanelTabKey } from './AccountHelper' import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository' import { ApiTokenSection } from './api-token-section/ApiTokenSection' +import { AccountInfoSection } from './account-info-section/AccountInfoSection' import styles from './Account.module.scss' const tabsKeys = AccountHelper.ACCOUNT_PANEL_TABS_KEYS @@ -28,11 +29,10 @@ export const Account = ({ defaultActiveTabKey, userRepository }: AccountProps) =
- -
+ +
+ +
diff --git a/src/sections/account/account-info-section/AccountInfoSection.tsx b/src/sections/account/account-info-section/AccountInfoSection.tsx new file mode 100644 index 000000000..67546c06b --- /dev/null +++ b/src/sections/account/account-info-section/AccountInfoSection.tsx @@ -0,0 +1,35 @@ +import { Table } from '@iqss/dataverse-design-system' +import { useSession } from '@/sections/session/SessionContext' +import { useTranslation } from 'react-i18next' + +// TODO - Add verified email icon +// TODO - Edit account information +// TODO - Change password + +export const AccountInfoSection = () => { + const { t } = useTranslation('account', { keyPrefix: 'info' }) + const { user } = useSession() + + return ( + + + + + + + + + + + + + + + + + + + +
{t('username')}{user?.identifier}
{t('givenName')}{user?.firstName}
{t('familyName')}{user?.lastName}
{t('email')}{user?.email}
+ ) +} diff --git a/src/sections/layout/header/LoggedInHeaderActions.tsx b/src/sections/layout/header/LoggedInHeaderActions.tsx index 642a023e2..30a2212bd 100644 --- a/src/sections/layout/header/LoggedInHeaderActions.tsx +++ b/src/sections/layout/header/LoggedInHeaderActions.tsx @@ -57,6 +57,11 @@ export const LoggedInHeaderActions = ({ + + {t('navigation.accountInfo')} + diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx index 9f34a9234..79052aa20 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx @@ -11,6 +11,8 @@ import { Validator } from '@/shared/helpers/Validator' import { type ValidTokenNotLinkedAccountFormData } from './types' import { ValidTokenNotLinkedAccountFormHelper } from './ValidTokenNotLinkedAccountFormHelper' import { TermsOfUse } from '@/info/domain/models/TermsOfUse' +import { Route } from '@/sections/Route.enum' +import { AccountHelper } from '@/sections/account/AccountHelper' import styles from './FormFields.module.scss' // TODO:ME - Maybe we should redirect to a welcome page after success? ask if there is one, maybe not the case for this scenario @@ -68,7 +70,10 @@ export const FormFields = ({ formDefaultValues, termsOfUse }: FormFieldsProps) = .then(async () => { await refetchUserSession() - navigate('/') + // Navigate to Account - Account Information tab after successful registration + navigate( + `${Route.ACCOUNT}?${AccountHelper.ACCOUNT_PANEL_TAB_QUERY_KEY}=${AccountHelper.ACCOUNT_PANEL_TABS_KEYS.accountInformation}` + ) }) .catch((error: AxiosError) => { console.error({ error }) From 96096485d1ff30ace42ff6005ced3e81b2ee1057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 28 Nov 2024 15:30:04 -0300 Subject: [PATCH 54/97] feat: add account info tab stories --- src/stories/account/Account.stories.tsx | 46 ++---------- .../account/AccountPageMockUserRepository.ts | 39 ---------- ...pository.ts => UserMockErrorRepository.ts} | 4 +- ...sitory.ts => UserMockLoadingRepository.ts} | 4 +- src/stories/account/UserMockRepository.ts | 49 +++++++++++++ .../AccountInfoSection.stories.tsx | 29 ++++++++ .../ApiTokenSection.stories.tsx | 72 +++++++++++++++++++ 7 files changed, 160 insertions(+), 83 deletions(-) delete mode 100644 src/stories/account/AccountPageMockUserRepository.ts rename src/stories/account/{AccountPageMockErrorUserRepository.ts => UserMockErrorRepository.ts} (88%) rename src/stories/account/{AccountPageMockLoadingUserRepository.ts => UserMockLoadingRepository.ts} (72%) create mode 100644 src/stories/account/UserMockRepository.ts create mode 100644 src/stories/account/account-info-section/AccountInfoSection.stories.tsx create mode 100644 src/stories/account/api-token-section/ApiTokenSection.stories.tsx diff --git a/src/stories/account/Account.stories.tsx b/src/stories/account/Account.stories.tsx index 3d0817452..dec9a475a 100644 --- a/src/stories/account/Account.stories.tsx +++ b/src/stories/account/Account.stories.tsx @@ -4,9 +4,7 @@ import { WithI18next } from '../WithI18next' import { WithLayout } from '../WithLayout' import { WithLoggedInUser } from '../WithLoggedInUser' import { AccountHelper } from '../../sections/account/AccountHelper' -import { AccountPageMockUserRepository } from './AccountPageMockUserRepository' -import { AccountPageMockLoadingUserRepository } from './AccountPageMockLoadingUserRepository' -import { AccountPageMockErrorUserRepository } from './AccountPageMockErrorUserRepository' +import { UserMockRepository } from './UserMockRepository' const meta: Meta = { title: 'Pages/Account', @@ -21,52 +19,20 @@ const meta: Meta = { export default meta type Story = StoryObj -export const APITokenTabDefault: Story = { +export const AccountInformation: Story = { render: () => ( - ) -} - -export const APITokenTabLoading: Story = { - render: () => ( - ) } -export const APITokenTabError: Story = { +export const ApiTokenTab: Story = { render: () => ( ) } - -export const APITokenTabNoToken: Story = { - render: () => { - const noTokenRepository = new AccountPageMockUserRepository() - noTokenRepository.getCurrentApiToken = () => { - return new Promise((resolve) => { - setTimeout(() => { - resolve({ - apiToken: '', - expirationDate: new Date() - }) - }, 1_000) - }) - } - - return ( - - ) - } -} diff --git a/src/stories/account/AccountPageMockUserRepository.ts b/src/stories/account/AccountPageMockUserRepository.ts deleted file mode 100644 index fdbf578b1..000000000 --- a/src/stories/account/AccountPageMockUserRepository.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository' -import { TokenInfo } from '@/users/domain/models/TokenInfo' -import { User } from '@/users/domain/models/User' - -export class AccountPageMockUserRepository extends UserJSDataverseRepository { - getAuthenticated(): Promise { - return Promise.resolve({ - displayName: 'mockDisplayName', - persistentId: 'mockPersistentId', - firstName: 'mockFirstName', - lastName: 'mockLastName', - email: 'mockEmail', - affiliation: 'mockAffiliation', - superuser: true - }) - } - - removeAuthenticated(): Promise { - return Promise.resolve() - } - - getCurrentApiToken(): Promise { - return Promise.resolve({ - apiToken: 'mock api token', - expirationDate: new Date() - }) - } - - recreateApiToken(): Promise { - return Promise.resolve({ - apiToken: 'updated mock api token', - expirationDate: new Date() - }) - } - - deleteApiToken(): Promise { - return Promise.resolve() - } -} diff --git a/src/stories/account/AccountPageMockErrorUserRepository.ts b/src/stories/account/UserMockErrorRepository.ts similarity index 88% rename from src/stories/account/AccountPageMockErrorUserRepository.ts rename to src/stories/account/UserMockErrorRepository.ts index 7662eb4ed..af3a06fd9 100644 --- a/src/stories/account/AccountPageMockErrorUserRepository.ts +++ b/src/stories/account/UserMockErrorRepository.ts @@ -1,9 +1,9 @@ -import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository' +import { UserMockRepository } from './UserMockRepository' import { TokenInfo } from '@/users/domain/models/TokenInfo' import { User } from '@/users/domain/models/User' import { FakerHelper } from '@tests/component/shared/FakerHelper' -export class AccountPageMockErrorUserRepository extends UserJSDataverseRepository { +export class UserMockErrorRepository extends UserMockRepository { getAuthenticated(): Promise { return new Promise((_resolve, reject) => { setTimeout(() => { diff --git a/src/stories/account/AccountPageMockLoadingUserRepository.ts b/src/stories/account/UserMockLoadingRepository.ts similarity index 72% rename from src/stories/account/AccountPageMockLoadingUserRepository.ts rename to src/stories/account/UserMockLoadingRepository.ts index 57668a59f..275ec5196 100644 --- a/src/stories/account/AccountPageMockLoadingUserRepository.ts +++ b/src/stories/account/UserMockLoadingRepository.ts @@ -1,8 +1,8 @@ -import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository' +import { UserMockRepository } from './UserMockRepository' import { TokenInfo } from '@/users/domain/models/TokenInfo' import { User } from '@/users/domain/models/User' -export class AccountPageMockLoadingUserRepository extends UserJSDataverseRepository { +export class UserMockLoadingRepository extends UserMockRepository { getAuthenticated(): Promise { return new Promise(() => {}) } diff --git a/src/stories/account/UserMockRepository.ts b/src/stories/account/UserMockRepository.ts new file mode 100644 index 000000000..79252c2f3 --- /dev/null +++ b/src/stories/account/UserMockRepository.ts @@ -0,0 +1,49 @@ +import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository' +import { TokenInfo } from '@/users/domain/models/TokenInfo' +import { User } from '@/users/domain/models/User' +import { UserMother } from '@tests/component/users/domain/models/UserMother' +import { FakerHelper } from '@tests/component/shared/FakerHelper' + +export class UserMockRepository extends UserJSDataverseRepository { + getAuthenticated(): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(UserMother.create()) + }, FakerHelper.loadingTimout()) + }) + } + + removeAuthenticated(): Promise { + return Promise.resolve() + } + + getCurrentApiToken(): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + apiToken: 'mock api token', + expirationDate: new Date() + }) + }, FakerHelper.loadingTimout()) + }) + } + + recreateApiToken(): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + apiToken: 'updated mock api token', + expirationDate: new Date() + }) + }, FakerHelper.loadingTimout()) + }) + } + + deleteApiToken(): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, FakerHelper.loadingTimout()) + }) + } +} diff --git a/src/stories/account/account-info-section/AccountInfoSection.stories.tsx b/src/stories/account/account-info-section/AccountInfoSection.stories.tsx new file mode 100644 index 000000000..5d72f81cd --- /dev/null +++ b/src/stories/account/account-info-section/AccountInfoSection.stories.tsx @@ -0,0 +1,29 @@ +import { Meta, StoryObj } from '@storybook/react' +import { Account } from '@/sections/account/Account' +import { WithI18next } from '@/stories/WithI18next' +import { WithLayout } from '@/stories/WithLayout' +import { WithLoggedInUser } from '@/stories/WithLoggedInUser' +import { AccountHelper } from '@/sections/account/AccountHelper' +import { UserMockRepository } from '../UserMockRepository' + +const meta: Meta = { + title: 'Sections/Account Page/AccountInfoSection', + component: Account, + decorators: [WithI18next, WithLayout, WithLoggedInUser], + parameters: { + // Sets the delay for all stories. + chromatic: { delay: 15000, pauseAnimationAtEnd: true } + } +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => ( + + ) +} diff --git a/src/stories/account/api-token-section/ApiTokenSection.stories.tsx b/src/stories/account/api-token-section/ApiTokenSection.stories.tsx new file mode 100644 index 000000000..5a32165ec --- /dev/null +++ b/src/stories/account/api-token-section/ApiTokenSection.stories.tsx @@ -0,0 +1,72 @@ +import { Meta, StoryObj } from '@storybook/react' +import { Account } from '@/sections/account/Account' +import { WithI18next } from '@/stories/WithI18next' +import { WithLayout } from '@/stories/WithLayout' +import { WithLoggedInUser } from '@/stories/WithLoggedInUser' +import { AccountHelper } from '@/sections/account/AccountHelper' +import { UserMockRepository } from '../UserMockRepository' +import { UserMockLoadingRepository } from '../UserMockLoadingRepository' +import { UserMockErrorRepository } from '../UserMockErrorRepository' + +const meta: Meta = { + title: 'Sections/Account Page/ApiTokenSection', + component: Account, + decorators: [WithI18next, WithLayout, WithLoggedInUser], + parameters: { + // Sets the delay for all stories. + chromatic: { delay: 15000, pauseAnimationAtEnd: true } + } +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => ( + + ) +} + +export const Loading: Story = { + render: () => ( + + ) +} + +export const Error: Story = { + render: () => ( + + ) +} + +export const NoToken: Story = { + render: () => { + const noTokenRepository = new UserMockRepository() + noTokenRepository.getCurrentApiToken = () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + apiToken: '', + expirationDate: new Date() + }) + }, 1_000) + }) + } + + return ( + + ) + } +} From baa605d0d3f177a01f1a17e424a12ec6c53ed80f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 28 Nov 2024 15:40:36 -0300 Subject: [PATCH 55/97] test: account unit tests --- .../sections/account/Account.spec.tsx | 2 +- .../account/AccountInfoSection.spec.tsx | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 tests/component/sections/account/AccountInfoSection.spec.tsx diff --git a/tests/component/sections/account/Account.spec.tsx b/tests/component/sections/account/Account.spec.tsx index bd32411fd..4ecf68879 100644 --- a/tests/component/sections/account/Account.spec.tsx +++ b/tests/component/sections/account/Account.spec.tsx @@ -13,8 +13,8 @@ describe('Account', () => { cy.get('h1').should('contain.text', 'Account') cy.findByRole('tab', { name: 'My Data' }).should('exist').and('be.disabled') - cy.findByRole('tab', { name: 'Account Information' }).should('exist').and('be.disabled') cy.findByRole('tab', { name: 'Notifications' }).should('exist').and('be.disabled') + cy.findByRole('tab', { name: 'Account Information' }).should('exist') cy.findByRole('tab', { name: 'API Token' }).should('be.enabled') }) }) diff --git a/tests/component/sections/account/AccountInfoSection.spec.tsx b/tests/component/sections/account/AccountInfoSection.spec.tsx new file mode 100644 index 000000000..1fc9d6a9e --- /dev/null +++ b/tests/component/sections/account/AccountInfoSection.spec.tsx @@ -0,0 +1,32 @@ +import { AccountInfoSection } from '@/sections/account/account-info-section/AccountInfoSection' +import { UserMother } from '@tests/component/users/domain/models/UserMother' + +const testUser = UserMother.create() + +describe('AccountInfoSection', () => { + it('should display the user information', () => { + cy.mountAuthenticated() + + cy.findAllByRole('row').spread((usernameRow, givenNameRow, familyNameRow, emailRow) => { + cy.wrap(usernameRow).within(() => { + cy.findByText('Username').should('exist') + cy.findByText(testUser.identifier).should('exist') + }) + + cy.wrap(givenNameRow).within(() => { + cy.findByText('Given Name').should('exist') + cy.findByText(testUser.firstName).should('exist') + }) + + cy.wrap(familyNameRow).within(() => { + cy.findByText('Family Name').should('exist') + cy.findByText(testUser.lastName).should('exist') + }) + + cy.wrap(emailRow).within(() => { + cy.findByText('Email').should('exist') + cy.findByText(testUser.email).should('exist') + }) + }) + }) +}) From 084a85e401b8f2563d92cc803e8a265c762c9ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Nov 2024 09:28:11 -0300 Subject: [PATCH 56/97] test: auth-callback unit test --- src/sections/auth-callback/AuthCallback.tsx | 15 ++- .../auth-callback/AuthCallback.spec.tsx | 71 ++++++++++++++ .../e2e/sections/dataset/Dataset.spec.tsx | 1 - tests/support/commands.tsx | 93 +++++++++++-------- tests/support/component.ts | 20 +++- 5 files changed, 150 insertions(+), 50 deletions(-) create mode 100644 tests/component/sections/auth-callback/AuthCallback.spec.tsx diff --git a/src/sections/auth-callback/AuthCallback.tsx b/src/sections/auth-callback/AuthCallback.tsx index 5f3b0862a..ded4c535c 100644 --- a/src/sections/auth-callback/AuthCallback.tsx +++ b/src/sections/auth-callback/AuthCallback.tsx @@ -43,13 +43,18 @@ export const encodeReturnToPathInStateQueryParam = (returnToPath: string): strin export const decodeReturnToPathFromStateQueryParam = (stateQueryParam: string): string => { const decodedStateQueryParam = decodeURIComponent(stateQueryParam) - const parsedStateQueryParam = JSON.parse(decodedStateQueryParam) as unknown - if (isReturnToObject(parsedStateQueryParam)) { - return parsedStateQueryParam.returnTo - } + try { + const parsedStateQueryParam = JSON.parse(decodedStateQueryParam) as unknown + + if (isReturnToObject(parsedStateQueryParam)) { + return parsedStateQueryParam.returnTo + } - return '/' + return '/' + } catch (_error) { + return '/' + } } function isReturnToObject(obj: unknown): obj is AuthStateQueryParamValue { diff --git a/tests/component/sections/auth-callback/AuthCallback.spec.tsx b/tests/component/sections/auth-callback/AuthCallback.spec.tsx new file mode 100644 index 000000000..1e496b2c4 --- /dev/null +++ b/tests/component/sections/auth-callback/AuthCallback.spec.tsx @@ -0,0 +1,71 @@ +import { + AuthCallback, + encodeReturnToPathInStateQueryParam +} from '@/sections/auth-callback/AuthCallback' +import { QueryParamKey } from '@/sections/Route.enum' +import { AuthContextMother } from '@tests/component/auth/AuthContextMother' +import { AuthContext } from 'react-oauth2-code-pkce' +import { Route, Routes } from 'react-router-dom' + +const encodedReturnToProtectedPathMock = encodeReturnToPathInStateQueryParam('/protected') + +describe('AuthCallback Component', () => { + const renderComponent = (loginInProgress: boolean, searchParams = '') => { + cy.customMount( + {}, + logOut: () => {}, + loginInProgress: loginInProgress, + tokenData: AuthContextMother.createTokenData(), + idTokenData: AuthContextMother.createTokenData(), + error: null, + login: () => {} // 👈 deprecated + }}> + + } /> + Home
} /> + Protected
} /> + + , + [`/callback${searchParams}`] + ) + } + + it('redirects to home if state query param is missing', () => { + renderComponent(false) // No state param + cy.get('[data-cy="home-page"]').should('exist') + }) + + it('does not redirect while login is in progress', () => { + renderComponent(true, `?${QueryParamKey.AUTH_STATE}=${encodedReturnToProtectedPathMock}`) + + cy.get('[data-cy="protected-page"]').should('not.exist') + cy.findByTestId('app-loader').should('exist') + }) + + it('redirects to the intended path when state is present and has a returnTo property', () => { + renderComponent(false, `?${QueryParamKey.AUTH_STATE}=${encodedReturnToProtectedPathMock}`) + + cy.get('[data-cy="protected-page"]').should('exist') + cy.findByTestId('app-loader').should('not.exist') + }) + + // edge cases + it('redirects to home if state query param is just a string', () => { + renderComponent(false, `?${QueryParamKey.AUTH_STATE}=invalid`) + + cy.get('[data-cy="home-page"]').should('exist') + }) + + it('redirects to home if state query param does not have a returnTo property', () => { + renderComponent( + false, + `?${QueryParamKey.AUTH_STATE}=${encodeURIComponent('{"invalid": "invalid"}')}` + ) + + cy.get('[data-cy="home-page"]').should('exist') + }) +}) diff --git a/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx b/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx index ee28e478a..55266d326 100644 --- a/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx +++ b/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx @@ -8,7 +8,6 @@ import { FileHelper } from '../../../shared/files/FileHelper' import moment from 'moment-timezone' import { CollectionHelper } from '../../../shared/collection/CollectionHelper' import { FILES_TAB_INFINITE_SCROLL_ENABLED } from '../../../../../src/sections/dataset/config' -import { DateHelper } from '@/shared/helpers/DateHelper' type Dataset = { datasetVersion: { metadataBlocks: { citation: { fields: { value: string }[] } } } diff --git a/tests/support/commands.tsx b/tests/support/commands.tsx index 95c92b04b..3f4c1baa3 100644 --- a/tests/support/commands.tsx +++ b/tests/support/commands.tsx @@ -43,7 +43,7 @@ import { ThemeProvider } from '@iqss/dataverse-design-system' import { ReactNode } from 'react' import { I18nextProvider } from 'react-i18next' import i18next from '../../src/i18n' -import { MemoryRouter } from 'react-router-dom' +import { Location, MemoryRouter } from 'react-router-dom' import { TestsUtils } from '@tests/e2e-integration/shared/TestsUtils' import { Utils } from '@/shared/helpers/Utils' import { OIDC_AUTH_CONFIG } from '@/config' @@ -51,47 +51,60 @@ import { SessionContext } from '@/sections/session/SessionContext' // Define your custom mount function -Cypress.Commands.add('customMount', (component: ReactNode) => { - return cy.mount( - - - {component} - - - ) -}) +export type RouterInitialEntry = string | Partial -Cypress.Commands.add('mountAuthenticated', (component: ReactNode) => { - return cy.customMount( - Promise.resolve(), - setUser: () => {}, - isLoadingUser: false, - sessionError: null, - refetchUserSession: () => Promise.resolve() - }}> - {component} - - ) -}) +Cypress.Commands.add( + 'customMount', + (component: ReactNode, initialEntries?: RouterInitialEntry[]) => { + return cy.mount( + + + {component} + + + ) + } +) -Cypress.Commands.add('mountSuperuser', (component: ReactNode) => { - return cy.customMount( - Promise.resolve(), - setUser: () => {}, - isLoadingUser: false, - sessionError: null, - refetchUserSession: () => Promise.resolve() - }}> - {component} - - ) -}) +Cypress.Commands.add( + 'mountAuthenticated', + (component: ReactNode, initialEntries?: RouterInitialEntry[]) => { + return cy.customMount( + Promise.resolve(), + setUser: () => {}, + isLoadingUser: false, + sessionError: null, + refetchUserSession: () => Promise.resolve() + }}> + {component} + , + initialEntries + ) + } +) + +Cypress.Commands.add( + 'mountSuperuser', + (component: ReactNode, initialEntries?: RouterInitialEntry[]) => { + return cy.customMount( + Promise.resolve(), + setUser: () => {}, + isLoadingUser: false, + sessionError: null, + refetchUserSession: () => Promise.resolve() + }}> + {component} + , + initialEntries + ) + } +) Cypress.Commands.add('login', () => { cy.visit('/spa/') diff --git a/tests/support/component.ts b/tests/support/component.ts index 13f20f361..44f019eb7 100644 --- a/tests/support/component.ts +++ b/tests/support/component.ts @@ -21,7 +21,9 @@ import 'react-loading-skeleton/dist/skeleton.css' // Alternatively you can use CommonJS syntax: // require('./commands') -import { mount } from 'cypress/react18' +import { mount, MountReturn } from 'cypress/react18' +import { RouterInitialEntry } from './commands' +import { ReactNode } from 'react' // Augment the Cypress namespace to include type definitions for // your custom command. @@ -33,9 +35,19 @@ declare global { namespace Cypress { interface Chainable { mount: typeof mount - customMount: typeof mount - mountAuthenticated: typeof mount - mountSuperuser: typeof mount + // customMount: typeof mount + customMount: ( + component: ReactNode, + initialEntries?: RouterInitialEntry[] + ) => Cypress.Chainable + mountAuthenticated: ( + component: ReactNode, + initialEntries?: RouterInitialEntry[] + ) => Cypress.Chainable + mountSuperuser: ( + component: ReactNode, + initialEntries?: RouterInitialEntry[] + ) => Cypress.Chainable login(): Chainable logout(): Chainable> compareDate(date: Date, expectedDate: Date): Chainable From ec97eea1a6c0c7889dc23170670415c6049b18de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Nov 2024 12:44:25 -0300 Subject: [PATCH 57/97] test: sign up and form tests --- src/sections/sign-up/SignUp.tsx | 33 ++- .../FormFields.tsx | 29 +- tests/component/auth/AuthContextMother.ts | 76 ++++- .../sections/sign-up/SignUp.spec.tsx | 69 +++++ .../ValidTokenNotLinkedAccountForm.spec.tsx | 262 ++++++++++++++++++ ...lidTokenNotLinkedAccountFormHelper.spec.ts | 197 +++++++++++++ .../DatasetJSDataverseRepository.spec.ts | 2 +- 7 files changed, 634 insertions(+), 34 deletions(-) create mode 100644 tests/component/sections/sign-up/SignUp.spec.tsx create mode 100644 tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx create mode 100644 tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountFormHelper.spec.ts diff --git a/src/sections/sign-up/SignUp.tsx b/src/sections/sign-up/SignUp.tsx index 8a48e8eac..c185a9daa 100644 --- a/src/sections/sign-up/SignUp.tsx +++ b/src/sections/sign-up/SignUp.tsx @@ -8,6 +8,25 @@ import styles from './SignUp.module.scss' // TODO:ME - All use cases will return same error message so this is blocking us for making requests to other public use cases like get root collection, should work removing access token from localstorage but we need it for future call // TODO:ME - How to handle 401 Unauthorized {"status":"ERROR","message":"Unauthorized bearer token."} globally, maybe redirect to oidc login page? +// TODO:ME - Maybe we should redirect to a welcome page after success? ask if there is one, maybe not the case for this scenario +// TODO:ME - Ask about the format of the terms of use, html string? just text string? what is shown in the box if there is just a url string ? +// TODO:ME - Ask about logout when clicking the Cancel button because of the BEARER_TOKEN_IS_VALID_BUT_NOT_LINKED_MESSAGE error +// TODO:ME - JS-DATAVERSE use case for registration +// TODO:ME - JS-DATAVERSE use case for getting the terms of use? how to avoid sending token in this case? + +/* + This is the expected response from the server after succesfull registration, will help for js-dataverse-client-javascript + const resp = { + data: { + status: 'OK', + data: { + message: 'User registered.' + } + }, + status: 200, + statusText: 'OK' + } +*/ interface SignUpProps { dataverseInfoRepository: DataverseInfoRepository @@ -28,18 +47,26 @@ export const SignUp = ({
{!hasValidTokenButNotLinkedAccount && ( - {t('createAccount.alertText')} + + {t('createAccount.alertText')} + )} {hasValidTokenButNotLinkedAccount && ( <> - + {t('hasValidTokenButNotLinkedAccount.alertText')} - {t('aboutPrefilledFields')} + + {t('aboutPrefilledFields')} + )} diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx index 79052aa20..680020f9a 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx @@ -15,27 +15,6 @@ import { Route } from '@/sections/Route.enum' import { AccountHelper } from '@/sections/account/AccountHelper' import styles from './FormFields.module.scss' -// TODO:ME - Maybe we should redirect to a welcome page after success? ask if there is one, maybe not the case for this scenario -// TODO:ME - Ask about the format of the terms of use, html string? just text string? what is shown in the box if there is just a url string ? -// TODO:ME - Ask about logout when clicking the Cancel button because of the BEARER_TOKEN_IS_VALID_BUT_NOT_LINKED_MESSAGE error -// TODO:ME - JS-DATAVERSE use case for registration -// TODO:ME - Show the registration write error message to the user after encapsulating this call in js-dataverse -// TODO:ME - JS-DATAVERSE use case for getting the terms of use? how to avoid sending token in this case? - -/* - This is the expected response from the server after succesfull registration, will help for js-dataverse-client-javascript - const resp = { - data: { - status: 'OK', - data: { - message: 'User registered.' - } - }, - status: 200, - statusText: 'OK' - } -*/ - interface FormFieldsProps { formDefaultValues: ValidTokenNotLinkedAccountFormData termsOfUse: TermsOfUse @@ -75,9 +54,11 @@ export const FormFields = ({ formDefaultValues, termsOfUse }: FormFieldsProps) = `${Route.ACCOUNT}?${AccountHelper.ACCOUNT_PANEL_TAB_QUERY_KEY}=${AccountHelper.ACCOUNT_PANEL_TABS_KEYS.accountInformation}` ) }) - .catch((error: AxiosError) => { - console.error({ error }) - }) + .catch( + /* istanbul ignore next */ (error: AxiosError) => { + console.error({ error }) + } + ) } // If the user cancels the registration, we should logout the user and redirect to the home page. diff --git a/tests/component/auth/AuthContextMother.ts b/tests/component/auth/AuthContextMother.ts index f5e231176..d398ee615 100644 --- a/tests/component/auth/AuthContextMother.ts +++ b/tests/component/auth/AuthContextMother.ts @@ -1,10 +1,46 @@ +export type FakeTokenData = { + name: string + preferred_username: string + given_name: string + family_name: string + email: string + email_verified: boolean + exp: number + iat: number + auth_time: number + jti: string + iss: string + aud: string + sub: string + typ: string + azp: string + session_state: string + acr: string + realm_access: { + roles: string[] + } + resource_access: { + account: { + roles: string[] + } + } + scope: string + sid: string +} + export class AuthContextMother { static createToken() { return 'some.fake.token' } - static createTokenData() { + static createTokenData(props?: Partial): FakeTokenData { return { + name: 'Dataverse User', + preferred_username: 'user', + given_name: 'Dataverse', + family_name: 'User', + email: 'dataverse-user@mailinator.com', + email_verified: true, exp: 1732803352, iat: 1732803052, auth_time: 1732799407, @@ -26,12 +62,40 @@ export class AuthContextMother { }, scope: 'openid profile email', sid: 'some-fake-sid-number', - email_verified: true, + ...props + } + } + + static createTokenDataWithNoUsernameEmailFirstnameAndLastname( + props?: Partial< + Omit + > + ): Omit { + return { name: 'Dataverse User', - preferred_username: 'user', - given_name: 'Dataverse', - family_name: 'User', - email: 'dataverse-user@mailinator.com' + email_verified: true, + exp: 1732803352, + iat: 1732803052, + auth_time: 1732799407, + jti: 'some-fake-jti-number', + iss: 'http://localhost:8000/realms/test', + aud: 'account', + sub: 'some-fake-sub-number', + typ: 'Bearer', + azp: 'test', + session_state: 'some-fake-session-state-number', + acr: '0', + realm_access: { + roles: ['default-roles-test', 'offline_access', 'uma_authorization'] + }, + resource_access: { + account: { + roles: ['manage-account', 'manage-account-links', 'view-profile'] + } + }, + scope: 'openid profile email', + sid: 'some-fake-sid-number', + ...props } } } diff --git a/tests/component/sections/sign-up/SignUp.spec.tsx b/tests/component/sections/sign-up/SignUp.spec.tsx new file mode 100644 index 000000000..8fc29fec0 --- /dev/null +++ b/tests/component/sections/sign-up/SignUp.spec.tsx @@ -0,0 +1,69 @@ +import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' +import { SignUp } from '@/sections/sign-up/SignUp' +import { AuthContextMother } from '@tests/component/auth/AuthContextMother' +import { AuthContext } from 'react-oauth2-code-pkce' + +const dataverseInfoRepository: DataverseInfoRepository = {} as DataverseInfoRepository + +describe('SignUp', () => { + beforeEach(() => { + dataverseInfoRepository.getTermsOfUse = cy.stub().resolves('Terms of use') + }) + + it('renders the valid token not linked account form and correct alerts when hasValidTokenButNotLinkedAccount prop is true', () => { + cy.customMount( + {}, + logOut: () => {}, + loginInProgress: false, + tokenData: AuthContextMother.createTokenData(), + idTokenData: AuthContextMother.createTokenData(), + error: null, + login: () => {} // 👈 deprecated + }}> + + + ) + + cy.findByTestId('valid-token-not-linked-account-alert-text').should('exist') + cy.findByTestId('valid-token-not-linked-account-about-prefilled-fields').should('exist') + cy.findByTestId('default-create-account-alert-text').should('not.exist') + + cy.findByTestId('valid-token-not-linked-account-form').should('exist') + }) + + // For now we are only rendering the form for the case when theres is a valid token but is not linked to any account, but we prepare the test for other cases + it('renders the default create account alert when hasValidTokenButNotLinkedAccount prop is false', () => { + cy.customMount( + {}, + logOut: () => {}, + loginInProgress: false, + tokenData: AuthContextMother.createTokenData(), + idTokenData: AuthContextMother.createTokenData(), + error: null, + login: () => {} // 👈 deprecated + }}> + + + ) + + cy.findByTestId('default-create-account-alert-text').should('exist') + cy.findByTestId('valid-token-not-linked-account-alert-text').should('not.exist') + cy.findByTestId('valid-token-not-linked-account-about-prefilled-fields').should('not.exist') + + cy.findByTestId('valid-token-not-linked-account-form').should('not.exist') + }) +}) diff --git a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx new file mode 100644 index 000000000..b216d606e --- /dev/null +++ b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx @@ -0,0 +1,262 @@ +// import { axiosInstance } from '@/axiosInstance' +import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' +import { ValidTokenNotLinkedAccountForm } from '@/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm' +import { AuthContextMother } from '@tests/component/auth/AuthContextMother' +import { AuthContext } from 'react-oauth2-code-pkce' + +const dataverseInfoRepository: DataverseInfoRepository = {} as DataverseInfoRepository + +const termsOfUseMock = 'Terms of use' +const mockUserName = 'mockUserName' +const mockFirstName = 'mockFirstName' +const mockLastName = 'mockLastName' +const mockEmail = 'mockEmail@email.com' + +const successfullStaticResponse = { + data: { + status: 'OK', + data: { + message: 'User registered.' + } + }, + status: 200, + statusText: 'OK' +} + +describe('ValidTokenNotLinkedAccountForm', () => { + beforeEach(() => { + dataverseInfoRepository.getTermsOfUse = cy.stub().resolves(termsOfUseMock) + + // Intercept axios register call to avoid real API call + cy.intercept('POST', '/api/users/register', successfullStaticResponse).as('registerUserCall') + }) + + describe('form fields correct values', () => { + it('renders the form fields with the correct default values when tokenData has preferred username, given name, family name and email', () => { + cy.customMount( + {}, + logOut: () => {}, + loginInProgress: false, + tokenData: AuthContextMother.createTokenData({ + preferred_username: mockUserName, + given_name: mockFirstName, + family_name: mockLastName, + email: mockEmail + }), + idTokenData: AuthContextMother.createTokenData(), + error: null, + login: () => {} // 👈 deprecated + }}> + + + ) + + cy.findByLabelText('Username').should('have.value', mockUserName) + cy.findByLabelText('Given Name').should('have.value', mockFirstName) + cy.findByLabelText('Family Name').should('have.value', mockLastName) + cy.findByLabelText('Email').should('have.value', mockEmail) + cy.findByText(termsOfUseMock).should('exist') + }) + + it('renders the form fields with the correct default values when tokenData does not have preferred username, given name, family name and email', () => { + cy.customMount( + {}, + logOut: () => {}, + loginInProgress: false, + tokenData: AuthContextMother.createTokenDataWithNoUsernameEmailFirstnameAndLastname(), + idTokenData: AuthContextMother.createTokenDataWithNoUsernameEmailFirstnameAndLastname(), + error: null, + login: () => {} // 👈 deprecated + }}> + + + ) + + cy.findByLabelText('Username').should('have.value', '') + cy.findByLabelText('Given Name').should('have.value', '') + cy.findByLabelText('Family Name').should('have.value', '') + cy.findByLabelText('Email').should('have.value', '') + cy.findByText(termsOfUseMock).should('exist') + }) + }) + + describe('submit form with correct data', () => { + it('submits the form with the correct data when tokenData has preferred username, given name, family name and email ', () => { + cy.customMount( + {}, + logOut: () => {}, + loginInProgress: false, + tokenData: AuthContextMother.createTokenData({ + preferred_username: mockUserName, + given_name: mockFirstName, + family_name: mockLastName, + email: mockEmail + }), + idTokenData: AuthContextMother.createTokenData({ + preferred_username: mockUserName, + given_name: mockFirstName, + family_name: mockLastName, + email: mockEmail + }), + error: null, + login: () => {} // 👈 deprecated + }}> + + + ) + + cy.findByLabelText( + 'I have read and accept the Dataverse General Terms of Use as outlined above.' + ).check({ force: true }) + + cy.findByRole('button', { name: 'Create Account' }).click() + + cy.wait('@registerUserCall').then((interception) => { + const requestBody = interception.request.body as Record + + expect(requestBody).to.deep.equal({ + termsAccepted: true + }) + }) + }) + + it('submits the form with the correct data when tokenData does not have preferred username, given name, family name and email', () => { + cy.customMount( + {}, + logOut: () => {}, + loginInProgress: false, + tokenData: AuthContextMother.createTokenDataWithNoUsernameEmailFirstnameAndLastname(), + idTokenData: AuthContextMother.createTokenDataWithNoUsernameEmailFirstnameAndLastname(), + error: null, + login: () => {} // 👈 deprecated + }}> + + + ) + + const newMockUserName = 'newMockUserName' + const newMockFirstName = 'newMockFirstName' + const newMockLastName = 'newMockLastName' + const newMockEmail = 'newMockEmail@email.com' + + // Assert that submit button is disabled if terms are not accepted + cy.findByRole('button', { name: 'Create Account' }).should('be.disabled') + + cy.findByLabelText( + 'I have read and accept the Dataverse General Terms of Use as outlined above.' + ).check({ force: true }) + + // Uncheck and then check again to test validation error from terms not accepted + cy.findByLabelText( + 'I have read and accept the Dataverse General Terms of Use as outlined above.' + ).uncheck({ force: true }) + + cy.findByText( + 'Please check the box to indicate your acceptance of the General Terms of Use.' + ).should('exist') + + cy.findByLabelText( + 'I have read and accept the Dataverse General Terms of Use as outlined above.' + ).check({ force: true }) + + cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') + + cy.findByRole('button', { name: 'Create Account' }).click() + + // Assert that the form has errors in Username and Email fields + cy.findByText('Username is required.').should('exist') + cy.findByText('Given Name is required.').should('exist') + cy.findByText('Family Name is required.').should('exist') + cy.findByText('Email is required.').should('exist') + + // Type a bad username to check validation first + cy.findByLabelText('Username').type('bad Username') + cy.findByText('Username is invalid.').should('exist') + cy.findByLabelText('Username').clear() + cy.findByLabelText('Username').type(newMockUserName) + + // Fill the rest of the fields + cy.findByLabelText('Given Name').type(newMockFirstName) + cy.findByLabelText('Family Name').type(newMockLastName) + cy.findByLabelText('Email').type(newMockEmail) + + cy.findByRole('button', { name: 'Create Account' }).click() + + cy.wait('@registerUserCall').then((interception) => { + const requestBody = interception.request.body as Record + + expect(requestBody).to.deep.equal({ + termsAccepted: true, + username: newMockUserName, + emailAddress: newMockEmail, + firstName: newMockFirstName, + lastName: newMockLastName + }) + }) + }) + }) + + it('shows no terms message when there are no terms of use', () => { + dataverseInfoRepository.getTermsOfUse = cy.stub().resolves(null) + + cy.customMount( + {}, + logOut: () => {}, + loginInProgress: false, + tokenData: AuthContextMother.createTokenData(), + idTokenData: AuthContextMother.createTokenData(), + error: null, + login: () => {} // 👈 deprecated + }}> + + + ) + + cy.findByText('There are no Terms of Use for this Dataverse installation.').should('exist') + }) + + it('logOut function is called when clicking the Cancel button', () => { + const logOut = cy.stub().as('logOut') + + cy.customMount( + {}, + logOut, + loginInProgress: false, + tokenData: AuthContextMother.createTokenData(), + idTokenData: AuthContextMother.createTokenData(), + error: null, + login: () => {} // 👈 deprecated + }}> + + + ) + + cy.findByRole('button', { name: 'Cancel' }).click() + + cy.get('@logOut').should('have.been.called') + }) +}) diff --git a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountFormHelper.spec.ts b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountFormHelper.spec.ts new file mode 100644 index 000000000..278f3fc41 --- /dev/null +++ b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountFormHelper.spec.ts @@ -0,0 +1,197 @@ +import { + OIDC_STANDARD_CLAIMS, + ValidTokenNotLinkedAccountFormData +} from '@/sections/sign-up/valid-token-not-linked-account-form/types' +import { ValidTokenNotLinkedAccountFormHelper } from '@/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountFormHelper' + +describe('ValidTokenNotLinkedAccountFormHelper', () => { + describe('getTokenDataValue', () => { + it('returns undefined if tokenData is undefined', () => { + const key = 'preferred_username' + const expectedTypeOfKey = 'string' + const tokenData = undefined + + const result = ValidTokenNotLinkedAccountFormHelper.getTokenDataValue( + key, + expectedTypeOfKey, + tokenData + ) + + expect(result).to.be.undefined + }) + + it('returns undefined if key is not in tokenData', () => { + const key = 'preferred_username' + const expectedTypeOfKey = 'string' + const tokenData = {} + + const result = ValidTokenNotLinkedAccountFormHelper.getTokenDataValue( + key, + expectedTypeOfKey, + tokenData + ) + + expect(result).to.be.undefined + }) + + it('returns undefined if value type is not expectedTypeOfKey', () => { + const key = 'preferred_username' + const expectedTypeOfKey = 'string' + const tokenData = { + [OIDC_STANDARD_CLAIMS.PREFERRED_USERNAME]: 1 + } + + const result = ValidTokenNotLinkedAccountFormHelper.getTokenDataValue( + key, + expectedTypeOfKey, + tokenData + ) + + expect(result).to.be.undefined + }) + + it('returns key value if key exists and value type is expectedTypeOfKey', () => { + const key = 'preferred_username' + const expectedTypeOfKey = 'string' + const tokenData = { + [OIDC_STANDARD_CLAIMS.PREFERRED_USERNAME]: 'mockUserName' + } + + const result = ValidTokenNotLinkedAccountFormHelper.getTokenDataValue( + key, + expectedTypeOfKey, + tokenData + ) + + expect(result).to.equal('mockUserName') + }) + }) + + describe('defineRegistrationDTOProperties', () => { + it('does not add form data token related properties to registrationDTO when tokenData have them present', () => { + const formData: ValidTokenNotLinkedAccountFormData = { + username: 'mockUserName', + firstName: 'mockFirstName', + lastName: 'mockLastName', + emailAddress: 'mockEmail', + position: '', + affiliation: '', + termsAccepted: true + } + + const tokenData = { + [OIDC_STANDARD_CLAIMS.PREFERRED_USERNAME]: 'mockUserName', + [OIDC_STANDARD_CLAIMS.GIVEN_NAME]: 'mockFirstName', + [OIDC_STANDARD_CLAIMS.FAMILY_NAME]: 'mockFirstName', + [OIDC_STANDARD_CLAIMS.EMAIL]: 'mockEmail' + } + + const result = ValidTokenNotLinkedAccountFormHelper.defineRegistrationDTOProperties( + formData, + tokenData + ) + + expect(result).to.deep.equal({ termsAccepted: true }) + }) + + it('does add form data token related properties to registrationDTO when tokenData does not have them present', () => { + const formData: ValidTokenNotLinkedAccountFormData = { + username: 'mockUserName', + firstName: 'mockFirstName', + lastName: 'mockLastName', + emailAddress: 'mockEmail', + position: '', + affiliation: '', + termsAccepted: true + } + + const tokenData = { + [OIDC_STANDARD_CLAIMS.GIVEN_NAME]: 'mockFirstName', + [OIDC_STANDARD_CLAIMS.EMAIL]: 'mockEmail' + } + + const result = ValidTokenNotLinkedAccountFormHelper.defineRegistrationDTOProperties( + formData, + tokenData + ) + + expect(result).to.deep.equal({ + termsAccepted: true, + username: 'mockUserName', + lastName: 'mockLastName' + }) + }) + + it('adds position and affiliation from formData to registrationDTO if they have value', () => { + const formData: ValidTokenNotLinkedAccountFormData = { + username: 'mockUserName', + firstName: 'mockFirstName', + lastName: 'mockLastName', + emailAddress: 'mockEmail', + position: 'mockPosition', + affiliation: 'mockAffiliation', + termsAccepted: true + } + + const tokenData = { + [OIDC_STANDARD_CLAIMS.PREFERRED_USERNAME]: 'mockUserName', + [OIDC_STANDARD_CLAIMS.GIVEN_NAME]: 'mockFirstName', + [OIDC_STANDARD_CLAIMS.FAMILY_NAME]: 'mockFirstName', + [OIDC_STANDARD_CLAIMS.EMAIL]: 'mockEmail' + } + + const result = ValidTokenNotLinkedAccountFormHelper.defineRegistrationDTOProperties( + formData, + tokenData + ) + + expect(result).to.deep.equal({ + position: 'mockPosition', + affiliation: 'mockAffiliation', + termsAccepted: true + }) + }) + + it('returns registrationDTO with all properties from formData when tokenData is undefined', () => { + const formData: ValidTokenNotLinkedAccountFormData = { + username: 'mockUserName', + firstName: 'mockFirstName', + lastName: 'mockLastName', + emailAddress: 'mockEmail', + position: 'mockPosition', + affiliation: 'mockAffiliation', + termsAccepted: true + } + + const tokenData = undefined + + const result = ValidTokenNotLinkedAccountFormHelper.defineRegistrationDTOProperties( + formData, + tokenData + ) + + expect(result).to.deep.equal(formData) + }) + + it('returns registrationDTO with all properties from formData when tokenData is empty', () => { + const formData: ValidTokenNotLinkedAccountFormData = { + username: 'mockUserName', + firstName: 'mockFirstName', + lastName: 'mockLastName', + emailAddress: 'mockEmail', + position: 'mockPosition', + affiliation: 'mockAffiliation', + termsAccepted: true + } + + const tokenData = {} + + const result = ValidTokenNotLinkedAccountFormHelper.defineRegistrationDTOProperties( + formData, + tokenData + ) + + expect(result).to.deep.equal(formData) + }) + }) +}) diff --git a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts index d3d40a9d5..30e5447b9 100644 --- a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts @@ -135,7 +135,7 @@ const datasetData = (persistentId: string, versionId: number) => { } } -// TODO:ME Some tests are failing, for dataset permissions is not matching +// TODO:ME Some tests are failing because dataset permissions is not matching const collectionId = 'DatasetJSDataverseRepository' const datasetRepository = new DatasetJSDataverseRepository() From c158b7078ac4fbab49ec6a556fb4cc603abf8122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Nov 2024 15:41:52 -0300 Subject: [PATCH 58/97] chore: upgrade js-dataverse --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2e5556840..8604aa5e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-pr224.045ca23", + "@iqss/dataverse-client-javascript": "2.0.0-pr224.a5634ad", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -3675,9 +3675,9 @@ }, "node_modules/@iqss/dataverse-client-javascript": { "name": "@IQSS/dataverse-client-javascript", - "version": "2.0.0-pr224.045ca23", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr224.045ca23/50a1ad77bca4ca2a696085f56b79fecd2e72196b", - "integrity": "sha512-D0M5qaB9tpw9hFS9DzhLgLNW4wZj5+biHpUw1mmCfm2q93WL09mHZuHkB0dCnfGTVSj3TkVoSM3Xp8Cq/cdB+Q==", + "version": "2.0.0-pr224.a5634ad", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr224.a5634ad/efb3fffecf1baf5f686a2183d9703bde584e3f18", + "integrity": "sha512-HupVa//v5Q1Fo3Ln5Ia1pqiDhjR6Zl9MmydOgvxgZROdGNeuFoI6unWd/GghEbaPMSM5QHvtE5oHhQQjBlFnOg==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", diff --git a/package.json b/package.json index b76b1b83a..7a5a22bd0 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-pr224.045ca23", + "@iqss/dataverse-client-javascript": "2.0.0-pr224.a5634ad", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", From bddfcac10eb6f8b076774b7bcf97ebb7be505bcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Nov 2024 16:41:34 -0300 Subject: [PATCH 59/97] feat: use register use case instead of axios --- public/locales/en/signUp.json | 5 +- src/sections/sign-up/SignUp.tsx | 8 +- src/sections/sign-up/SignUpFactory.tsx | 3 + .../FormFields.module.scss | 4 + .../FormFields.tsx | 68 +++++++------- .../ValidTokenNotLinkedAccountForm.tsx | 11 ++- .../useSubmitUser.ts | 89 +++++++++++++++++++ .../domain/repositories/UserRepository.tsx | 2 + src/users/domain/useCases/DTOs/UserDTO.ts | 9 ++ src/users/domain/useCases/registerUser.ts | 6 ++ .../repositories/UserJSDataverseRepository.ts | 10 ++- 11 files changed, 174 insertions(+), 41 deletions(-) create mode 100644 src/sections/sign-up/valid-token-not-linked-account-form/useSubmitUser.ts create mode 100644 src/users/domain/useCases/DTOs/UserDTO.ts create mode 100644 src/users/domain/useCases/registerUser.ts diff --git a/public/locales/en/signUp.json b/public/locales/en/signUp.json index 346a652bc..97e82de13 100644 --- a/public/locales/en/signUp.json +++ b/public/locales/en/signUp.json @@ -50,5 +50,8 @@ "noTerms": "There are no Terms of Use for this Dataverse installation." } }, - "submit": "Create Account" + "submit": "Create Account", + "status": { + "success": "User account created successfully." + } } diff --git a/src/sections/sign-up/SignUp.tsx b/src/sections/sign-up/SignUp.tsx index c185a9daa..e56e84e5c 100644 --- a/src/sections/sign-up/SignUp.tsx +++ b/src/sections/sign-up/SignUp.tsx @@ -2,6 +2,7 @@ import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Alert, Tabs } from '@iqss/dataverse-design-system' import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' +import { UserRepository } from '@/users/domain/repositories/UserRepository' import { useLoading } from '../loading/LoadingContext' import { ValidTokenNotLinkedAccountForm } from './valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm' import styles from './SignUp.module.scss' @@ -29,11 +30,13 @@ import styles from './SignUp.module.scss' */ interface SignUpProps { + userRepository: UserRepository dataverseInfoRepository: DataverseInfoRepository hasValidTokenButNotLinkedAccount: boolean } export const SignUp = ({ + userRepository, dataverseInfoRepository, hasValidTokenButNotLinkedAccount }: SignUpProps) => { @@ -79,7 +82,10 @@ export const SignUp = ({
{hasValidTokenButNotLinkedAccount && ( - + )}
diff --git a/src/sections/sign-up/SignUpFactory.tsx b/src/sections/sign-up/SignUpFactory.tsx index 3444d7fba..23810054e 100644 --- a/src/sections/sign-up/SignUpFactory.tsx +++ b/src/sections/sign-up/SignUpFactory.tsx @@ -3,8 +3,10 @@ import { useSearchParams } from 'react-router-dom' import { SignUp } from './SignUp' import { QueryParamKey } from '../Route.enum' import { DataverseInfoJSDataverseRepository } from '@/info/infrastructure/repositories/DataverseInfoJSDataverseRepository' +import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository' const dataverseInfoRepository = new DataverseInfoJSDataverseRepository() +const userRepository = new UserJSDataverseRepository() export class SignUpFactory { static create(): ReactElement { @@ -21,6 +23,7 @@ function SignUpWithSearchParams() { return ( ) diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.module.scss b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.module.scss index ff7c48956..a45f3d935 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.module.scss +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.module.scss @@ -1,3 +1,7 @@ +.form-container { + scroll-margin-top: 62px; +} + .form-group { margin-bottom: 1rem; diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx index 680020f9a..30bedd452 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx @@ -1,32 +1,28 @@ -import { useContext } from 'react' -import { useNavigate } from 'react-router-dom' +import { useContext, useRef } from 'react' import { AuthContext } from 'react-oauth2-code-pkce' import { Controller, FormProvider, useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' -import { AxiosError } from 'axios' -import { axiosInstance } from '@/axiosInstance' -import { Button, Col, Form, Stack } from '@iqss/dataverse-design-system' -import { useSession } from '@/sections/session/SessionContext' +import { Alert, Button, Col, Form, Stack } from '@iqss/dataverse-design-system' +import { UserRepository } from '@/users/domain/repositories/UserRepository' import { Validator } from '@/shared/helpers/Validator' import { type ValidTokenNotLinkedAccountFormData } from './types' -import { ValidTokenNotLinkedAccountFormHelper } from './ValidTokenNotLinkedAccountFormHelper' import { TermsOfUse } from '@/info/domain/models/TermsOfUse' -import { Route } from '@/sections/Route.enum' -import { AccountHelper } from '@/sections/account/AccountHelper' +import { SubmissionStatus, useSubmitUser } from './useSubmitUser' import styles from './FormFields.module.scss' interface FormFieldsProps { + userRepository: UserRepository formDefaultValues: ValidTokenNotLinkedAccountFormData termsOfUse: TermsOfUse } -export const FormFields = ({ formDefaultValues, termsOfUse }: FormFieldsProps) => { - const navigate = useNavigate() - const { refetchUserSession } = useSession() +export const FormFields = ({ userRepository, formDefaultValues, termsOfUse }: FormFieldsProps) => { const { tokenData, logOut: oidcLogout } = useContext(AuthContext) const { t } = useTranslation('signUp') const { t: tShared } = useTranslation('shared') + const formContainerRef = useRef(null) + const isUsernameRequired = formDefaultValues.username === '' const isEmailRequired = formDefaultValues.emailAddress === '' const isFirstNameRequired = formDefaultValues.firstName === '' @@ -37,34 +33,22 @@ export const FormFields = ({ formDefaultValues, termsOfUse }: FormFieldsProps) = defaultValues: formDefaultValues }) - const submitForm = (formData: ValidTokenNotLinkedAccountFormData) => { - // We wont send properties that are already present in the tokenData, those are the disabled/readonly fields - const registrationDTO = ValidTokenNotLinkedAccountFormHelper.defineRegistrationDTOProperties( - formData, - tokenData - ) - - axiosInstance - .post('/api/users/register', registrationDTO) - .then(async () => { - await refetchUserSession() - - // Navigate to Account - Account Information tab after successful registration - navigate( - `${Route.ACCOUNT}?${AccountHelper.ACCOUNT_PANEL_TAB_QUERY_KEY}=${AccountHelper.ACCOUNT_PANEL_TABS_KEYS.accountInformation}` - ) - }) - .catch( - /* istanbul ignore next */ (error: AxiosError) => { - console.error({ error }) - } - ) - } + const { submissionStatus, submitError, submitForm } = useSubmitUser( + userRepository, + onSubmitUserError, + tokenData + ) // If the user cancels the registration, we should logout the user and redirect to the home page. // This is to avoid sending the valid bearer token and receiving the same BEARER_TOKEN_IS_VALID_BUT_NOT_LINKED_MESSAGE error const handleCancel = () => oidcLogout() + function onSubmitUserError() { + if (formContainerRef.current) { + formContainerRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' }) + } + } + const userNameRules = { required: isUsernameRequired ? t('fields.username.required') : false, validate: (value: string) => { @@ -105,13 +89,25 @@ export const FormFields = ({ formDefaultValues, termsOfUse }: FormFieldsProps) = const hasAcceptedTheTermsOfUse = form.watch('termsAccepted') return ( -
+
{/*
{t('aboutPrefilledFields')}
*/} + {submissionStatus === SubmissionStatus.Errored && ( + + {submitError} + + )} + + {submissionStatus === SubmissionStatus.SubmitComplete && ( + + {t('status.success')} + + )} + { const { tokenData } = useContext(AuthContext) @@ -59,5 +62,11 @@ export const ValidTokenNotLinkedAccountForm = ({ return } - return + return ( + + ) } diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/useSubmitUser.ts b/src/sections/sign-up/valid-token-not-linked-account-form/useSubmitUser.ts new file mode 100644 index 000000000..8c41a1cf4 --- /dev/null +++ b/src/sections/sign-up/valid-token-not-linked-account-form/useSubmitUser.ts @@ -0,0 +1,89 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { type TTokenData } from 'react-oauth2-code-pkce/dist/types' +import { WriteError } from '@iqss/dataverse-client-javascript' +import { UserRepository } from '@/users/domain/repositories/UserRepository' +import { UserDTO } from '@/users/domain/useCases/DTOs/UserDTO' +import { registerUser } from '@/users/domain/useCases/registerUser' +import { useSession } from '@/sections/session/SessionContext' +import { AccountHelper } from '@/sections/account/AccountHelper' +import { Route } from '@/sections/Route.enum' +import { ValidTokenNotLinkedAccountFormHelper } from './ValidTokenNotLinkedAccountFormHelper' +import { ValidTokenNotLinkedAccountFormData } from './types' +import { JSDataverseWriteErrorHandler } from '@/shared/helpers/JSDataverseWriteErrorHandler' + +export enum SubmissionStatus { + NotSubmitted = 'NotSubmitted', + IsSubmitting = 'IsSubmitting', + SubmitComplete = 'SubmitComplete', + Errored = 'Errored' +} + +type UseSubmitUserReturnType = + | { + submissionStatus: + | SubmissionStatus.NotSubmitted + | SubmissionStatus.IsSubmitting + | SubmissionStatus.SubmitComplete + submitForm: (formData: ValidTokenNotLinkedAccountFormData) => void + submitError: null + } + | { + submissionStatus: SubmissionStatus.Errored + submitForm: (formData: ValidTokenNotLinkedAccountFormData) => void + submitError: string + } + +export const useSubmitUser = ( + userRepository: UserRepository, + onSubmitErrorCallback: () => void, + tokenData?: TTokenData +): UseSubmitUserReturnType => { + const { refetchUserSession } = useSession() + const navigate = useNavigate() + + const [submissionStatus, setSubmissionStatus] = useState( + SubmissionStatus.NotSubmitted + ) + const [submitError, setSubmitError] = useState(null) + + const submitForm = (formData: ValidTokenNotLinkedAccountFormData): void => { + setSubmissionStatus(SubmissionStatus.IsSubmitting) + + // We wont send properties that are already present in the tokenData, those are the disabled/readonly fields + const registrationDTO: UserDTO = + ValidTokenNotLinkedAccountFormHelper.defineRegistrationDTOProperties(formData, tokenData) + + // TODO:ME - Ask Guillermo, when sending username or preferred_username even if it is in tokenData the endpoint is not failing. + // Also sending position property as 3 and not a string is ok? + // Ask team about this, should not be merged to 6.5 until fixed + + registerUser(userRepository, registrationDTO) + .then(async () => { + setSubmitError(null) + setSubmissionStatus(SubmissionStatus.SubmitComplete) + + await refetchUserSession() + + // Navigate to Account - Account Information tab after successful registration + navigate( + `${Route.ACCOUNT}?${AccountHelper.ACCOUNT_PANEL_TAB_QUERY_KEY}=${AccountHelper.ACCOUNT_PANEL_TABS_KEYS.accountInformation}` + ) + }) + .catch((err: WriteError) => { + const error = new JSDataverseWriteErrorHandler(err) + const formattedError = error.getReasonWithoutStatusCode() ?? error.getErrorMessage() + + setSubmitError(formattedError) + setSubmissionStatus(SubmissionStatus.Errored) + + onSubmitErrorCallback() + }) + } + + return { + submissionStatus, + submitForm, + submitError + } as UseSubmitUserReturnType +} diff --git a/src/users/domain/repositories/UserRepository.tsx b/src/users/domain/repositories/UserRepository.tsx index 77295dedc..d65d5e1f8 100644 --- a/src/users/domain/repositories/UserRepository.tsx +++ b/src/users/domain/repositories/UserRepository.tsx @@ -1,5 +1,6 @@ import { User } from '../models/User' import { TokenInfo } from '../.././domain/models/TokenInfo' +import { UserDTO } from '../useCases/DTOs/UserDTO' export interface UserRepository { getAuthenticated: () => Promise @@ -7,4 +8,5 @@ export interface UserRepository { getCurrentApiToken: () => Promise recreateApiToken: () => Promise deleteApiToken: () => Promise + register: (user: UserDTO) => Promise } diff --git a/src/users/domain/useCases/DTOs/UserDTO.ts b/src/users/domain/useCases/DTOs/UserDTO.ts new file mode 100644 index 000000000..1f1bf9977 --- /dev/null +++ b/src/users/domain/useCases/DTOs/UserDTO.ts @@ -0,0 +1,9 @@ +export interface UserDTO { + username?: string + firstName?: string + lastName?: string + emailAddress?: string + position?: string + affiliation?: string + termsAccepted: boolean +} diff --git a/src/users/domain/useCases/registerUser.ts b/src/users/domain/useCases/registerUser.ts new file mode 100644 index 000000000..49c3ba680 --- /dev/null +++ b/src/users/domain/useCases/registerUser.ts @@ -0,0 +1,6 @@ +import { UserRepository } from '../repositories/UserRepository' +import { UserDTO } from './DTOs/UserDTO' + +export function registerUser(userRepository: UserRepository, userDTO: UserDTO): Promise { + return userRepository.register(userDTO) +} diff --git a/src/users/infrastructure/repositories/UserJSDataverseRepository.ts b/src/users/infrastructure/repositories/UserJSDataverseRepository.ts index d3409dffa..8988fe0c3 100644 --- a/src/users/infrastructure/repositories/UserJSDataverseRepository.ts +++ b/src/users/infrastructure/repositories/UserJSDataverseRepository.ts @@ -6,10 +6,12 @@ import { getCurrentAuthenticatedUser, getCurrentApiToken, recreateCurrentApiToken, - deleteCurrentApiToken -} from '@iqss/dataverse-client-javascript/dist/users' + deleteCurrentApiToken, + registerUser +} from '@iqss/dataverse-client-javascript' import { logout, ReadError, WriteError } from '@iqss/dataverse-client-javascript' import { JSUserMapper } from '../mappers/JSUserMapper' +import { UserDTO } from '@/users/domain/useCases/DTOs/UserDTO' interface ApiTokenInfoPayload { apiToken: string @@ -55,4 +57,8 @@ export class UserJSDataverseRepository implements UserRepository { deleteApiToken(): Promise { return deleteCurrentApiToken.execute() } + + register(user: UserDTO): Promise { + return registerUser.execute(user) + } } From 63a7cf99873d18e22bc92aadb41d786021b54761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Nov 2024 16:51:30 -0300 Subject: [PATCH 60/97] test: use user repo instead of axios --- .../sections/sign-up/SignUp.spec.tsx | 4 ++ .../ValidTokenNotLinkedAccountForm.spec.tsx | 65 +++++++++++-------- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/tests/component/sections/sign-up/SignUp.spec.tsx b/tests/component/sections/sign-up/SignUp.spec.tsx index 8fc29fec0..f359d17f8 100644 --- a/tests/component/sections/sign-up/SignUp.spec.tsx +++ b/tests/component/sections/sign-up/SignUp.spec.tsx @@ -1,9 +1,11 @@ import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' import { SignUp } from '@/sections/sign-up/SignUp' +import { UserRepository } from '@/users/domain/repositories/UserRepository' import { AuthContextMother } from '@tests/component/auth/AuthContextMother' import { AuthContext } from 'react-oauth2-code-pkce' const dataverseInfoRepository: DataverseInfoRepository = {} as DataverseInfoRepository +const userRepository: UserRepository = {} as UserRepository describe('SignUp', () => { beforeEach(() => { @@ -25,6 +27,7 @@ describe('SignUp', () => { login: () => {} // 👈 deprecated }}> @@ -54,6 +57,7 @@ describe('SignUp', () => { login: () => {} // 👈 deprecated }}> diff --git a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx index b216d606e..369027d91 100644 --- a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx +++ b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx @@ -1,10 +1,12 @@ -// import { axiosInstance } from '@/axiosInstance' +import { AuthContext } from 'react-oauth2-code-pkce' import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' +import { UserRepository } from '@/users/domain/repositories/UserRepository' import { ValidTokenNotLinkedAccountForm } from '@/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm' import { AuthContextMother } from '@tests/component/auth/AuthContextMother' -import { AuthContext } from 'react-oauth2-code-pkce' +import { UserDTO } from '@/users/domain/useCases/DTOs/UserDTO' const dataverseInfoRepository: DataverseInfoRepository = {} as DataverseInfoRepository +const userRepository: UserRepository = {} as UserRepository const termsOfUseMock = 'Terms of use' const mockUserName = 'mockUserName' @@ -12,23 +14,10 @@ const mockFirstName = 'mockFirstName' const mockLastName = 'mockLastName' const mockEmail = 'mockEmail@email.com' -const successfullStaticResponse = { - data: { - status: 'OK', - data: { - message: 'User registered.' - } - }, - status: 200, - statusText: 'OK' -} - describe('ValidTokenNotLinkedAccountForm', () => { beforeEach(() => { dataverseInfoRepository.getTermsOfUse = cy.stub().resolves(termsOfUseMock) - - // Intercept axios register call to avoid real API call - cy.intercept('POST', '/api/users/register', successfullStaticResponse).as('registerUserCall') + userRepository.register = cy.stub().as('registerUser').resolves() }) describe('form fields correct values', () => { @@ -51,7 +40,10 @@ describe('ValidTokenNotLinkedAccountForm', () => { error: null, login: () => {} // 👈 deprecated }}> - + ) @@ -76,7 +68,10 @@ describe('ValidTokenNotLinkedAccountForm', () => { error: null, login: () => {} // 👈 deprecated }}> - + ) @@ -113,7 +108,10 @@ describe('ValidTokenNotLinkedAccountForm', () => { error: null, login: () => {} // 👈 deprecated }}> - + ) @@ -123,10 +121,11 @@ describe('ValidTokenNotLinkedAccountForm', () => { cy.findByRole('button', { name: 'Create Account' }).click() - cy.wait('@registerUserCall').then((interception) => { - const requestBody = interception.request.body as Record + cy.get('@registerUser').should((spy) => { + const registerUserSpy = spy as unknown as Cypress.Agent + const userDTO = registerUserSpy.getCall(0).args[0] as UserDTO - expect(requestBody).to.deep.equal({ + expect(userDTO).to.deep.equal({ termsAccepted: true }) }) @@ -146,7 +145,10 @@ describe('ValidTokenNotLinkedAccountForm', () => { error: null, login: () => {} // 👈 deprecated }}> - + ) @@ -198,10 +200,11 @@ describe('ValidTokenNotLinkedAccountForm', () => { cy.findByRole('button', { name: 'Create Account' }).click() - cy.wait('@registerUserCall').then((interception) => { - const requestBody = interception.request.body as Record + cy.get('@registerUser').should((spy) => { + const registerUserSpy = spy as unknown as Cypress.Agent + const userDTO = registerUserSpy.getCall(0).args[0] as UserDTO - expect(requestBody).to.deep.equal({ + expect(userDTO).to.deep.equal({ termsAccepted: true, username: newMockUserName, emailAddress: newMockEmail, @@ -228,7 +231,10 @@ describe('ValidTokenNotLinkedAccountForm', () => { error: null, login: () => {} // 👈 deprecated }}> - + ) @@ -251,7 +257,10 @@ describe('ValidTokenNotLinkedAccountForm', () => { error: null, login: () => {} // 👈 deprecated }}> - + ) From 148cc26cb0f018e87f4d7f712bfc8718eff191ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Nov 2024 16:59:42 -0300 Subject: [PATCH 61/97] feat: use user repo in stories --- src/sections/sign-up/SignUp.tsx | 16 ---------------- src/stories/account/Account.stories.tsx | 2 +- .../AccountInfoSection.stories.tsx | 2 +- .../ApiTokenSection.stories.tsx | 6 +++--- .../user}/UserMockErrorRepository.ts | 8 ++++++++ .../user}/UserMockLoadingRepository.ts | 4 ++++ .../user}/UserMockRepository.ts | 9 +++++++++ src/stories/sign-up/SignUp.stories.tsx | 5 +++-- 8 files changed, 29 insertions(+), 23 deletions(-) rename src/stories/{account => shared-mock-repositories/user}/UserMockErrorRepository.ts (86%) rename src/stories/{account => shared-mock-repositories/user}/UserMockLoadingRepository.ts (90%) rename src/stories/{account => shared-mock-repositories/user}/UserMockRepository.ts (85%) diff --git a/src/sections/sign-up/SignUp.tsx b/src/sections/sign-up/SignUp.tsx index e56e84e5c..dfb3c18c7 100644 --- a/src/sections/sign-up/SignUp.tsx +++ b/src/sections/sign-up/SignUp.tsx @@ -8,27 +8,11 @@ import { ValidTokenNotLinkedAccountForm } from './valid-token-not-linked-account import styles from './SignUp.module.scss' // TODO:ME - All use cases will return same error message so this is blocking us for making requests to other public use cases like get root collection, should work removing access token from localstorage but we need it for future call -// TODO:ME - How to handle 401 Unauthorized {"status":"ERROR","message":"Unauthorized bearer token."} globally, maybe redirect to oidc login page? // TODO:ME - Maybe we should redirect to a welcome page after success? ask if there is one, maybe not the case for this scenario // TODO:ME - Ask about the format of the terms of use, html string? just text string? what is shown in the box if there is just a url string ? // TODO:ME - Ask about logout when clicking the Cancel button because of the BEARER_TOKEN_IS_VALID_BUT_NOT_LINKED_MESSAGE error -// TODO:ME - JS-DATAVERSE use case for registration // TODO:ME - JS-DATAVERSE use case for getting the terms of use? how to avoid sending token in this case? -/* - This is the expected response from the server after succesfull registration, will help for js-dataverse-client-javascript - const resp = { - data: { - status: 'OK', - data: { - message: 'User registered.' - } - }, - status: 200, - statusText: 'OK' - } -*/ - interface SignUpProps { userRepository: UserRepository dataverseInfoRepository: DataverseInfoRepository diff --git a/src/stories/account/Account.stories.tsx b/src/stories/account/Account.stories.tsx index dec9a475a..69373bfd8 100644 --- a/src/stories/account/Account.stories.tsx +++ b/src/stories/account/Account.stories.tsx @@ -4,7 +4,7 @@ import { WithI18next } from '../WithI18next' import { WithLayout } from '../WithLayout' import { WithLoggedInUser } from '../WithLoggedInUser' import { AccountHelper } from '../../sections/account/AccountHelper' -import { UserMockRepository } from './UserMockRepository' +import { UserMockRepository } from '../shared-mock-repositories/user/UserMockRepository' const meta: Meta = { title: 'Pages/Account', diff --git a/src/stories/account/account-info-section/AccountInfoSection.stories.tsx b/src/stories/account/account-info-section/AccountInfoSection.stories.tsx index 5d72f81cd..3d2b9b9f5 100644 --- a/src/stories/account/account-info-section/AccountInfoSection.stories.tsx +++ b/src/stories/account/account-info-section/AccountInfoSection.stories.tsx @@ -4,7 +4,7 @@ import { WithI18next } from '@/stories/WithI18next' import { WithLayout } from '@/stories/WithLayout' import { WithLoggedInUser } from '@/stories/WithLoggedInUser' import { AccountHelper } from '@/sections/account/AccountHelper' -import { UserMockRepository } from '../UserMockRepository' +import { UserMockRepository } from '../../shared-mock-repositories/user/UserMockRepository' const meta: Meta = { title: 'Sections/Account Page/AccountInfoSection', diff --git a/src/stories/account/api-token-section/ApiTokenSection.stories.tsx b/src/stories/account/api-token-section/ApiTokenSection.stories.tsx index 5a32165ec..5be44de8f 100644 --- a/src/stories/account/api-token-section/ApiTokenSection.stories.tsx +++ b/src/stories/account/api-token-section/ApiTokenSection.stories.tsx @@ -4,9 +4,9 @@ import { WithI18next } from '@/stories/WithI18next' import { WithLayout } from '@/stories/WithLayout' import { WithLoggedInUser } from '@/stories/WithLoggedInUser' import { AccountHelper } from '@/sections/account/AccountHelper' -import { UserMockRepository } from '../UserMockRepository' -import { UserMockLoadingRepository } from '../UserMockLoadingRepository' -import { UserMockErrorRepository } from '../UserMockErrorRepository' +import { UserMockRepository } from '../../shared-mock-repositories/user/UserMockRepository' +import { UserMockLoadingRepository } from '../../shared-mock-repositories/user/UserMockLoadingRepository' +import { UserMockErrorRepository } from '../../shared-mock-repositories/user/UserMockErrorRepository' const meta: Meta = { title: 'Sections/Account Page/ApiTokenSection', diff --git a/src/stories/account/UserMockErrorRepository.ts b/src/stories/shared-mock-repositories/user/UserMockErrorRepository.ts similarity index 86% rename from src/stories/account/UserMockErrorRepository.ts rename to src/stories/shared-mock-repositories/user/UserMockErrorRepository.ts index af3a06fd9..5dfc1f816 100644 --- a/src/stories/account/UserMockErrorRepository.ts +++ b/src/stories/shared-mock-repositories/user/UserMockErrorRepository.ts @@ -43,4 +43,12 @@ export class UserMockErrorRepository extends UserMockRepository { }, FakerHelper.loadingTimout()) }) } + + register(): Promise { + return new Promise((_resolve, reject) => { + setTimeout(() => { + reject('Something went wrong registering the user. Try again later.') + }, FakerHelper.loadingTimout()) + }) + } } diff --git a/src/stories/account/UserMockLoadingRepository.ts b/src/stories/shared-mock-repositories/user/UserMockLoadingRepository.ts similarity index 90% rename from src/stories/account/UserMockLoadingRepository.ts rename to src/stories/shared-mock-repositories/user/UserMockLoadingRepository.ts index 275ec5196..63ccbeb34 100644 --- a/src/stories/account/UserMockLoadingRepository.ts +++ b/src/stories/shared-mock-repositories/user/UserMockLoadingRepository.ts @@ -22,4 +22,8 @@ export class UserMockLoadingRepository extends UserMockRepository { deleteApiToken(): Promise { return new Promise(() => {}) } + + register(): Promise { + return new Promise(() => {}) + } } diff --git a/src/stories/account/UserMockRepository.ts b/src/stories/shared-mock-repositories/user/UserMockRepository.ts similarity index 85% rename from src/stories/account/UserMockRepository.ts rename to src/stories/shared-mock-repositories/user/UserMockRepository.ts index 79252c2f3..111a3f5cd 100644 --- a/src/stories/account/UserMockRepository.ts +++ b/src/stories/shared-mock-repositories/user/UserMockRepository.ts @@ -3,6 +3,7 @@ import { TokenInfo } from '@/users/domain/models/TokenInfo' import { User } from '@/users/domain/models/User' import { UserMother } from '@tests/component/users/domain/models/UserMother' import { FakerHelper } from '@tests/component/shared/FakerHelper' +import { UserDTO } from '@/users/domain/useCases/DTOs/UserDTO' export class UserMockRepository extends UserJSDataverseRepository { getAuthenticated(): Promise { @@ -46,4 +47,12 @@ export class UserMockRepository extends UserJSDataverseRepository { }, FakerHelper.loadingTimout()) }) } + + register(_user: UserDTO): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, FakerHelper.loadingTimout()) + }) + } } diff --git a/src/stories/sign-up/SignUp.stories.tsx b/src/stories/sign-up/SignUp.stories.tsx index 82773b97e..cea400d08 100644 --- a/src/stories/sign-up/SignUp.stories.tsx +++ b/src/stories/sign-up/SignUp.stories.tsx @@ -5,8 +5,7 @@ import { SignUp } from '@/sections/sign-up/SignUp' import { DataverseInfoMockRepository } from '../shared-mock-repositories/info/DataverseInfoMockRepository' import { DataverseInfoMockLoadingRepository } from '../shared-mock-repositories/info/DataverseInfoMockLoadingkRepository' import { WithOIDCAuthContext } from '../WithOIDCAuthContext' - -// TODO:ME - After implementing register use case in js-dataverse, we should mock the register function here also. +import { UserMockRepository } from '../shared-mock-repositories/user/UserMockRepository' const meta: Meta = { title: 'Pages/Sign Up', @@ -23,6 +22,7 @@ type Story = StoryObj export const ValidTokenWithNotLinkedAccount: Story = { render: () => ( @@ -32,6 +32,7 @@ export const ValidTokenWithNotLinkedAccount: Story = { export const Loading: Story = { render: () => ( From 58a43294634e5a1c1cba949ac03238918b449d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Nov 2024 17:11:50 -0300 Subject: [PATCH 62/97] feat: add position to User modal and show it in account info --- public/locales/en/account.json | 4 +++- .../account-info-section/AccountInfoSection.tsx | 12 ++++++++++++ src/users/domain/models/User.ts | 1 + src/users/infrastructure/mappers/JSUserMapper.ts | 1 + 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/public/locales/en/account.json b/public/locales/en/account.json index 23388c11b..35f9885be 100644 --- a/public/locales/en/account.json +++ b/public/locales/en/account.json @@ -19,6 +19,8 @@ "username": "Username", "givenName": "Given Name", "familyName": "Family Name", - "email": "Email" + "email": "Email", + "affiliation": "Affiliation", + "position": "Position" } } diff --git a/src/sections/account/account-info-section/AccountInfoSection.tsx b/src/sections/account/account-info-section/AccountInfoSection.tsx index 67546c06b..d9298723f 100644 --- a/src/sections/account/account-info-section/AccountInfoSection.tsx +++ b/src/sections/account/account-info-section/AccountInfoSection.tsx @@ -29,6 +29,18 @@ export const AccountInfoSection = () => { {t('email')} {user?.email} + {user?.affiliation && ( + + {t('affiliation')} + {user?.affiliation} + + )} + {user?.position && ( + + {t('position')} + {user?.position} + + )} ) diff --git a/src/users/domain/models/User.ts b/src/users/domain/models/User.ts index 30ccf24c7..e1ef71da3 100644 --- a/src/users/domain/models/User.ts +++ b/src/users/domain/models/User.ts @@ -7,4 +7,5 @@ export interface User { email: string identifier: string affiliation?: string + position?: string } diff --git a/src/users/infrastructure/mappers/JSUserMapper.ts b/src/users/infrastructure/mappers/JSUserMapper.ts index a69850d8e..4db5350b9 100644 --- a/src/users/infrastructure/mappers/JSUserMapper.ts +++ b/src/users/infrastructure/mappers/JSUserMapper.ts @@ -9,6 +9,7 @@ export class JSUserMapper { firstName: authenticatedUser.firstName, lastName: authenticatedUser.lastName, email: authenticatedUser.email, + position: authenticatedUser.position, affiliation: authenticatedUser.affiliation, superuser: authenticatedUser.superuser, identifier: this.removeAtSymbol(authenticatedUser.identifier) From 9b5d880ede2c27bf5f21bcf7d85e297509c019b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Nov 2024 17:21:42 -0300 Subject: [PATCH 63/97] chore: fix lint errors --- .../files/FileJSDataverseRepository.spec.ts | 11 +++++++---- .../integration/files/FileUpload.spec.ts | 12 +++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts index 8a2fb0641..e70596cdf 100644 --- a/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts @@ -152,11 +152,14 @@ const fileExpectedData = (id: number): File => { } describe('File JSDataverse Repository', () => { - before(() => { - TestsUtils.setup() - }) beforeEach(() => { - TestsUtils.login() + TestsUtils.login().then((token) => { + if (!token) { + throw new Error('Token not found after Keycloak login') + } + + cy.wrap(TestsUtils.setup(token)) + }) }) const compareMetadata = (fileMetadata: FileMetadata, expectedFileMetadata: FileMetadata) => { diff --git a/tests/e2e-integration/integration/files/FileUpload.spec.ts b/tests/e2e-integration/integration/files/FileUpload.spec.ts index 64b2ed688..1d9d4c4fe 100644 --- a/tests/e2e-integration/integration/files/FileUpload.spec.ts +++ b/tests/e2e-integration/integration/files/FileUpload.spec.ts @@ -13,12 +13,14 @@ const fileRepository = new FileJSDataverseRepository() const datasetRepository = new DatasetJSDataverseRepository() describe('DirectUpload', () => { - before(() => { - TestsUtils.setup() - }) - beforeEach(() => { - TestsUtils.login() + TestsUtils.login().then((token) => { + if (!token) { + throw new Error('Token not found after Keycloak login') + } + + cy.wrap(TestsUtils.setup(token)) + }) }) it('should upload file and add it to the dataset', async () => { From 75a59c54941289cfb060e87119cd251a41e2f903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 2 Dec 2024 09:28:24 -0300 Subject: [PATCH 64/97] feat: show created alert --- public/locales/en/account.json | 3 +- src/sections/account/Account.tsx | 5 +- src/sections/account/AccountFactory.tsx | 13 +++- .../AccountInfoSection.tsx | 67 ++++++++++--------- .../FormFields.tsx | 5 +- .../useSubmitUser.ts | 6 +- src/stories/account/Account.stories.tsx | 2 + .../AccountInfoSection.stories.tsx | 11 +++ .../ApiTokenSection.stories.tsx | 4 ++ .../sections/account/Account.spec.tsx | 1 + .../account/AccountInfoSection.spec.tsx | 10 ++- 11 files changed, 89 insertions(+), 38 deletions(-) diff --git a/public/locales/en/account.json b/public/locales/en/account.json index 35f9885be..8e1736508 100644 --- a/public/locales/en/account.json +++ b/public/locales/en/account.json @@ -21,6 +21,7 @@ "familyName": "Family Name", "email": "Email", "affiliation": "Affiliation", - "position": "Position" + "position": "Position", + "accountJustCreated": "Your account has been successfully created! Welcome to Dataverse!" } } diff --git a/src/sections/account/Account.tsx b/src/sections/account/Account.tsx index 1c74c7028..427dec3e9 100644 --- a/src/sections/account/Account.tsx +++ b/src/sections/account/Account.tsx @@ -11,9 +11,10 @@ const tabsKeys = AccountHelper.ACCOUNT_PANEL_TABS_KEYS interface AccountProps { defaultActiveTabKey: AccountPanelTabKey userRepository: UserJSDataverseRepository + accountCreated: boolean } -export const Account = ({ defaultActiveTabKey, userRepository }: AccountProps) => { +export const Account = ({ defaultActiveTabKey, userRepository, accountCreated }: AccountProps) => { const { t } = useTranslation('account') return ( @@ -31,7 +32,7 @@ export const Account = ({ defaultActiveTabKey, userRepository }: AccountProps) =
- +
diff --git a/src/sections/account/AccountFactory.tsx b/src/sections/account/AccountFactory.tsx index d4c2a6e20..f3ee049b7 100644 --- a/src/sections/account/AccountFactory.tsx +++ b/src/sections/account/AccountFactory.tsx @@ -1,5 +1,5 @@ import { ReactElement } from 'react' -import { useSearchParams } from 'react-router-dom' +import { useLocation, useSearchParams } from 'react-router-dom' import { AccountHelper } from './AccountHelper' import { Account } from './Account' import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository' @@ -15,6 +15,15 @@ export class AccountFactory { function AccountWithSearchParams() { const [searchParams] = useSearchParams() const defaultActiveTabKey = AccountHelper.defineSelectedTabKey(searchParams) + const location = useLocation() + const state = location.state as { accountCreated: boolean | undefined } | undefined + const accountCreated = state?.accountCreated ?? false - return + return ( + + ) } diff --git a/src/sections/account/account-info-section/AccountInfoSection.tsx b/src/sections/account/account-info-section/AccountInfoSection.tsx index d9298723f..19cbe39d5 100644 --- a/src/sections/account/account-info-section/AccountInfoSection.tsx +++ b/src/sections/account/account-info-section/AccountInfoSection.tsx @@ -1,47 +1,54 @@ -import { Table } from '@iqss/dataverse-design-system' +import { Alert, Table } from '@iqss/dataverse-design-system' import { useSession } from '@/sections/session/SessionContext' import { useTranslation } from 'react-i18next' // TODO - Add verified email icon // TODO - Edit account information // TODO - Change password +interface AccountInfoSectionProps { + accountCreated: boolean +} -export const AccountInfoSection = () => { +export const AccountInfoSection = ({ accountCreated }: AccountInfoSectionProps) => { const { t } = useTranslation('account', { keyPrefix: 'info' }) const { user } = useSession() return ( - - - - - - - - - - - - - - - - - - - {user?.affiliation && ( + <> + {accountCreated && {t('accountJustCreated')}} + +
{t('username')}{user?.identifier}
{t('givenName')}{user?.firstName}
{t('familyName')}{user?.lastName}
{t('email')}{user?.email}
+ + + + + + + + + - - + + - )} - {user?.position && ( - - + + - )} - -
{t('username')}{user?.identifier}
{t('givenName')}{user?.firstName}
{t('affiliation')}{user?.affiliation}{t('familyName')}{user?.lastName}
{t('position')}{user?.position}{t('email')}{user?.email}
+ {user?.affiliation && ( + + {t('affiliation')} + {user?.affiliation} + + )} + {user?.position && ( + + {t('position')} + {user?.position} + + )} + + + ) } diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx index 30bedd452..37ef97388 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx @@ -88,6 +88,9 @@ export const FormFields = ({ userRepository, formDefaultValues, termsOfUse }: Fo const hasAcceptedTheTermsOfUse = form.watch('termsAccepted') + const disableSubmitButton = + !hasAcceptedTheTermsOfUse || submissionStatus === SubmissionStatus.IsSubmitting + return (
{/*
@@ -303,7 +306,7 @@ export const FormFields = ({ userRepository, formDefaultValues, termsOfUse }: Fo - diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/useSubmitUser.ts b/src/sections/sign-up/valid-token-not-linked-account-form/useSubmitUser.ts index 8c41a1cf4..527283410 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/useSubmitUser.ts +++ b/src/sections/sign-up/valid-token-not-linked-account-form/useSubmitUser.ts @@ -67,7 +67,11 @@ export const useSubmitUser = ( // Navigate to Account - Account Information tab after successful registration navigate( - `${Route.ACCOUNT}?${AccountHelper.ACCOUNT_PANEL_TAB_QUERY_KEY}=${AccountHelper.ACCOUNT_PANEL_TABS_KEYS.accountInformation}` + `${Route.ACCOUNT}?${AccountHelper.ACCOUNT_PANEL_TAB_QUERY_KEY}=${AccountHelper.ACCOUNT_PANEL_TABS_KEYS.accountInformation}`, + { + state: { accountCreated: true }, + replace: true + } ) }) .catch((err: WriteError) => { diff --git a/src/stories/account/Account.stories.tsx b/src/stories/account/Account.stories.tsx index 69373bfd8..d9c45a8e2 100644 --- a/src/stories/account/Account.stories.tsx +++ b/src/stories/account/Account.stories.tsx @@ -24,6 +24,7 @@ export const AccountInformation: Story = { ) } @@ -33,6 +34,7 @@ export const ApiTokenTab: Story = { ) } diff --git a/src/stories/account/account-info-section/AccountInfoSection.stories.tsx b/src/stories/account/account-info-section/AccountInfoSection.stories.tsx index 3d2b9b9f5..8bf583664 100644 --- a/src/stories/account/account-info-section/AccountInfoSection.stories.tsx +++ b/src/stories/account/account-info-section/AccountInfoSection.stories.tsx @@ -24,6 +24,17 @@ export const Default: Story = { + ) +} + +export const AccountJustCreated: Story = { + render: () => ( + ) } diff --git a/src/stories/account/api-token-section/ApiTokenSection.stories.tsx b/src/stories/account/api-token-section/ApiTokenSection.stories.tsx index 5be44de8f..0ef804ab1 100644 --- a/src/stories/account/api-token-section/ApiTokenSection.stories.tsx +++ b/src/stories/account/api-token-section/ApiTokenSection.stories.tsx @@ -26,6 +26,7 @@ export const Default: Story = { ) } @@ -35,6 +36,7 @@ export const Loading: Story = { ) } @@ -44,6 +46,7 @@ export const Error: Story = { ) } @@ -66,6 +69,7 @@ export const NoToken: Story = { ) } diff --git a/tests/component/sections/account/Account.spec.tsx b/tests/component/sections/account/Account.spec.tsx index 4ecf68879..e407fea21 100644 --- a/tests/component/sections/account/Account.spec.tsx +++ b/tests/component/sections/account/Account.spec.tsx @@ -8,6 +8,7 @@ describe('Account', () => { ) diff --git a/tests/component/sections/account/AccountInfoSection.spec.tsx b/tests/component/sections/account/AccountInfoSection.spec.tsx index 1fc9d6a9e..64247c58a 100644 --- a/tests/component/sections/account/AccountInfoSection.spec.tsx +++ b/tests/component/sections/account/AccountInfoSection.spec.tsx @@ -5,7 +5,7 @@ const testUser = UserMother.create() describe('AccountInfoSection', () => { it('should display the user information', () => { - cy.mountAuthenticated() + cy.mountAuthenticated() cy.findAllByRole('row').spread((usernameRow, givenNameRow, familyNameRow, emailRow) => { cy.wrap(usernameRow).within(() => { @@ -29,4 +29,12 @@ describe('AccountInfoSection', () => { }) }) }) + + it('should display the account created alert', () => { + cy.mountAuthenticated() + + cy.findByText(/Your account has been successfully created! Welcome to Dataverse!/).should( + 'exist' + ) + }) }) From 855d5ffcd7222ad60a44072d3d65d94146d8f7f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 2 Dec 2024 17:01:29 -0300 Subject: [PATCH 65/97] tests: finish with e2e & integration tests --- src/sections/sign-up/SignUp.tsx | 2 +- .../ApiTokenInfoJSDataverseRepository.spec.ts | 48 ++- .../CollectionJSDataverseRepository.spec.ts | 24 +- .../DatasetJSDataverseRepository.spec.ts | 173 +++++---- .../files/FileJSDataverseRepository.spec.ts | 339 ++++++++++++++---- .../integration/files/FileUpload.spec.ts | 68 +++- ...DataverseInfoJSDataverseRepository.spec.ts | 21 +- ...dataBlockInfoJSDataverseRepository.spec.ts | 22 +- .../UserJSDataverseRepository.spec.ts | 51 ++- tests/e2e-integration/shared/TestsUtils.ts | 6 + tests/support/commands.tsx | 91 ++++- 11 files changed, 624 insertions(+), 221 deletions(-) diff --git a/src/sections/sign-up/SignUp.tsx b/src/sections/sign-up/SignUp.tsx index dfb3c18c7..c89ef98f7 100644 --- a/src/sections/sign-up/SignUp.tsx +++ b/src/sections/sign-up/SignUp.tsx @@ -30,7 +30,7 @@ export const SignUp = ({ useEffect(() => setIsLoading(false), [setIsLoading]) return ( -
+
{!hasValidTokenButNotLinkedAccount && ( diff --git a/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts index 162b2a329..cbb547f00 100644 --- a/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts @@ -1,8 +1,10 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { UserJSDataverseRepository } from '../../../../src/users/infrastructure/repositories/UserJSDataverseRepository' - import { TestsUtils } from '../../shared/TestsUtils' +import { ApiConfig } from '@iqss/dataverse-client-javascript' +import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' +import { DATAVERSE_BACKEND_URL, OIDC_AUTH_CONFIG } from '@/config' chai.use(chaiAsPromised) const expect = chai.expect @@ -10,26 +12,54 @@ const expect = chai.expect const userRepository = new UserJSDataverseRepository() describe('API Token Info JSDataverse Repository', () => { - before(() => TestsUtils.setup()) - beforeEach(() => TestsUtils.login()) + beforeEach(() => { + TestsUtils.login().then((token) => { + if (!token) { + throw new Error('Token not found after Keycloak login') + } + + cy.wrap(TestsUtils.setup(token)) + }) + }) + it('revoke the API token', async () => { + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + await expect(userRepository.deleteApiToken()).to.be.fulfilled }) it('create or recreate the API token and return the new token info', async () => { + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + const recreatedTokenInfo = await userRepository.recreateApiToken() - if (!recreatedTokenInfo) { - throw new Error('Failed to recreate API token') - } + expect(recreatedTokenInfo).to.have.property('apiToken').that.is.a('string') expect(recreatedTokenInfo).to.have.property('expirationDate').that.is.a('Date') }) it('fetch the current API token', async () => { + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + const tokenInfo = await userRepository.getCurrentApiToken() - if (!tokenInfo) { - throw new Error('API Token not found') - } + expect(tokenInfo).to.have.property('apiToken').that.is.a('string') expect(tokenInfo).to.have.property('expirationDate').that.is.a('Date') }) diff --git a/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts index 3c534e37a..9fd9ac932 100644 --- a/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts @@ -46,13 +46,11 @@ describe('Collection JSDataverse Repository', () => { const collectionResponse = await CollectionHelper.create('new-collection') // Change the api config to use bearer token - cy.wrap( - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) await collectionRepository.getById(collectionResponse.id).then((collection) => { @@ -69,13 +67,11 @@ describe('Collection JSDataverse Repository', () => { const collectionResponse = await CollectionHelper.create(uniqueCollectionId) // Change the api config to use bearer token - cy.wrap( - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) await collectionRepository.publish(collectionResponse.id) diff --git a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts index 30e5447b9..874236476 100644 --- a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts @@ -140,21 +140,15 @@ const datasetData = (persistentId: string, versionId: number) => { const collectionId = 'DatasetJSDataverseRepository' const datasetRepository = new DatasetJSDataverseRepository() describe('Dataset JSDataverse Repository', () => { - // before(() => { - // TestsUtils.setup() - // TestsUtils.login().then(() => CollectionHelper.createAndPublish(collectionId)) - // }) - // beforeEach(() => { - // TestsUtils.login() - // }) - beforeEach(() => { TestsUtils.login().then((token) => { if (!token) { throw new Error('Token not found after Keycloak login') } - cy.wrap(TestsUtils.setup(token)).then(() => CollectionHelper.createAndPublish(collectionId)) + cy.wrap(TestsUtils.setup(token)).then( + async () => await CollectionHelper.createAndPublish(collectionId) + ) }) }) @@ -162,13 +156,12 @@ describe('Dataset JSDataverse Repository', () => { const datasetResponse = await DatasetHelper.create(collectionId) // Change the api config to use bearer token - cy.wrap( - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) + + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) await datasetRepository @@ -192,28 +185,29 @@ describe('Dataset JSDataverse Repository', () => { }) }) - it.only('gets a published dataset by persistentId without user authentication', async () => { + it('gets a published dataset by persistentId without user authentication', async () => { + console.log('RUNNING TEST') const datasetResponse = await DatasetHelper.create(collectionId) await DatasetHelper.publish(datasetResponse.persistentId) await TestsUtils.wait(1500) - TestsUtils.logout() + // This is to simulate the user being logged out + cy.clearAllLocalStorage() + cy.clearAllCookies() // Change the api config to use bearer token - cy.wrap( - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) await datasetRepository .getByPersistentId(datasetResponse.persistentId, '1.0') .then((dataset) => { - console.log(dataset) + console.log({ dataset }) if (!dataset) { throw new Error('Dataset not found') } @@ -239,7 +233,6 @@ describe('Dataset JSDataverse Repository', () => { ) expect(dataset.metadataBlocks[0].fields.citationDate).not.to.exist - console.log(dataset.permissions) expect(dataset.permissions).to.deep.equal({ canDownloadFiles: true, canUpdateDataset: false, @@ -257,13 +250,12 @@ describe('Dataset JSDataverse Repository', () => { await TestsUtils.waitForNoLocks(datasetResponse.persistentId) // Change the api config to use bearer token - cy.wrap( - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) + + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) await datasetRepository @@ -294,6 +286,11 @@ describe('Dataset JSDataverse Repository', () => { expectedPublicationDate ) expect(dataset.metadataBlocks[0].fields.citationDate).not.to.exist + + console.log({ + datasetPermissions: dataset.permissions, + expected: datasetExpected.permissions + }) expect(dataset.permissions).to.deep.equal(datasetExpected.permissions) }) }) @@ -302,13 +299,12 @@ describe('Dataset JSDataverse Repository', () => { const datasetResponse = await DatasetHelper.create(collectionId) // Change the api config to use bearer token - cy.wrap( - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) + + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) await datasetRepository @@ -329,13 +325,12 @@ describe('Dataset JSDataverse Repository', () => { const privateUrlResponse = await DatasetHelper.createPrivateUrl(datasetResponse.id) // Change the api config to use bearer token - cy.wrap( - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) + + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) await datasetRepository.getByPrivateUrlToken(privateUrlResponse.token).then((dataset) => { @@ -359,13 +354,11 @@ describe('Dataset JSDataverse Repository', () => { await DatasetHelper.setCitationDateFieldType(datasetResponse.persistentId, 'dateOfDeposit') // Change the api config to use bearer token - cy.wrap( - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) await datasetRepository @@ -390,13 +383,12 @@ describe('Dataset JSDataverse Repository', () => { const paginationInfo = new DatasetPaginationInfo(1, 20) // Change the api config to use bearer token - cy.wrap( - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) + + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) return datasetRepository @@ -421,13 +413,12 @@ describe('Dataset JSDataverse Repository', () => { await DatasetHelper.deaccession(datasetResponse.id) // Change the api config to use bearer token - cy.wrap( - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) + + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) await datasetRepository.getByPersistentId(datasetResponse.persistentId).then((dataset) => { @@ -446,13 +437,12 @@ describe('Dataset JSDataverse Repository', () => { await DatasetHelper.lock(datasetResponse.id, DatasetLockReason.FINALIZE_PUBLICATION) // Change the api config to use bearer token - cy.wrap( - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) + + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) await datasetRepository @@ -464,9 +454,10 @@ describe('Dataset JSDataverse Repository', () => { const datasetExpected = datasetData(dataset.persistentId, dataset.version.id) expect(dataset.version.title).to.deep.equal(datasetExpected.title) + expect(dataset.locks).to.deep.equal([ { - userPersistentId: 'dataverseAdmin', + userPersistentId: TestsUtils.USER_USERNAME, reason: DatasetLockReason.FINALIZE_PUBLICATION } ]) @@ -475,13 +466,12 @@ describe('Dataset JSDataverse Repository', () => { it('creates a new dataset from DatasetDTO', async () => { // Change the api config to use bearer token - cy.wrap( - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) + + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) const datasetDTO: DatasetDTO = { @@ -516,17 +506,17 @@ describe('Dataset JSDataverse Repository', () => { expect(response.persistentId).to.exist }) }) + it('publishes a draft dataset', async () => { const datasetResponse = await DatasetHelper.create(collectionId) // Change the api config to use bearer token - cy.wrap( - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) + + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) await datasetRepository.publish(datasetResponse.persistentId).then((response) => { @@ -541,6 +531,7 @@ describe('Dataset JSDataverse Repository', () => { expect(datasetResponse?.version.publishingStatus).to.equal(DatasetPublishingStatus.RELEASED) }) }) + it.skip('publishes a new version of a previously released dataset', async () => { const datasetResponse = await DatasetHelper.createAndPublish(collectionId) // TODO: update dataset diff --git a/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts index e70596cdf..3a207744b 100644 --- a/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts @@ -31,6 +31,9 @@ import { } from '../../../../src/dataset/domain/models/Dataset' import { File } from '../../../../src/files/domain/models/File' import { FileIngest, FileIngestStatus } from '../../../../src/files/domain/models/FileIngest' +import { ApiConfig } from '@iqss/dataverse-client-javascript' +import { DATAVERSE_BACKEND_URL, OIDC_AUTH_CONFIG } from '@/config' +import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' const DRAFT_PARAM = DatasetNonNumericVersion.DRAFT chai.use(chaiAsPromised) @@ -179,13 +182,21 @@ describe('File JSDataverse Repository', () => { describe('Get all files by dataset persistentId', () => { it('gets all the files by dataset persistentId with the basic information', async () => { - const dataset = await DatasetHelper.createWithFiles(FileHelper.createMany(3)).then( - (datasetResponse) => - datasetRepository.getByPersistentId( - datasetResponse.persistentId, - DatasetNonNumericVersion.DRAFT - ) + const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(3)) + + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + + const dataset = await datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DatasetNonNumericVersion.DRAFT ) + if (!dataset) throw new Error('Dataset not found') await fileRepository @@ -210,14 +221,31 @@ describe('File JSDataverse Repository', () => { file: new Blob([new ArrayBuffer(expectedSize.value)], { type: 'text/csv' }), jsonData: JSON.stringify({ description: 'This is an example file' }) } - const dataset = await DatasetHelper.createWithFiles([fileData]).then((datasetResponse) => - datasetRepository.getByPersistentId( - datasetResponse.persistentId, - DatasetNonNumericVersion.DRAFT - ) + const datasetResponse = await DatasetHelper.createWithFiles([fileData]) + + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) + + const dataset = await datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DatasetNonNumericVersion.DRAFT + ) + if (!dataset) throw new Error('Dataset not found') + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + await fileRepository .getAllByDatasetPersistentId(dataset.persistentId, dataset.version) .then((files) => { @@ -226,18 +254,34 @@ describe('File JSDataverse Repository', () => { }) it('gets all the files by dataset persistentId after dataset publication', async () => { - const dataset = await DatasetHelper.createWithFiles(FileHelper.createMany(3)).then( - (datasetResponse) => - datasetRepository.getByPersistentId( - datasetResponse.persistentId, - DatasetNonNumericVersion.DRAFT - ) + const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(3)) + + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) + + const dataset = await datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DatasetNonNumericVersion.DRAFT + ) + if (!dataset) throw new Error('Dataset not found') await DatasetHelper.publish(dataset.persistentId) await TestsUtils.waitForNoLocks(dataset.persistentId) // Wait for the dataset to be published + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + await fileRepository .getAllByDatasetPersistentId( dataset.persistentId, @@ -266,11 +310,19 @@ describe('File JSDataverse Repository', () => { await DatasetHelper.publish(datasetResponse.persistentId) await TestsUtils.waitForNoLocks(datasetResponse.persistentId) // Wait for the dataset to be published + await DatasetHelper.deaccession(datasetResponse.id) + + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + const dataset = await datasetRepository.getByPersistentId(datasetResponse.persistentId) if (!dataset) throw new Error('Dataset not found') - await DatasetHelper.deaccession(datasetResponse.id) - await fileRepository .getAllByDatasetPersistentId( dataset.persistentId, @@ -295,12 +347,20 @@ describe('File JSDataverse Repository', () => { await DatasetHelper.publish(datasetResponse.persistentId) await TestsUtils.waitForNoLocks(datasetResponse.persistentId) // Wait for the dataset to be published - const dataset = await datasetRepository.getByPersistentId(datasetResponse.persistentId) - if (!dataset) throw new Error('Dataset not found') - await FileHelper.download(datasetResponse.files[0].id) await TestsUtils.wait(3000) // Wait for the file to be downloaded + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + + const dataset = await datasetRepository.getByPersistentId(datasetResponse.persistentId) + if (!dataset) throw new Error('Dataset not found') + await fileRepository .getAllByDatasetPersistentId(dataset.persistentId, dataset.version) .then((files) => { @@ -313,18 +373,26 @@ describe('File JSDataverse Repository', () => { const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(3)) if (!datasetResponse.files) throw new Error('Files not found') - const dataset = await datasetRepository.getByPersistentId( - datasetResponse.persistentId, - DatasetNonNumericVersion.DRAFT - ) - if (!dataset) throw new Error('Dataset not found') - const expectedLabels = [ { type: FileLabelType.CATEGORY, value: 'category' }, { type: FileLabelType.CATEGORY, value: 'category_2' } ] await FileHelper.addLabel(datasetResponse.files[0].id, expectedLabels) + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + + const dataset = await datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DatasetNonNumericVersion.DRAFT + ) + if (!dataset) throw new Error('Dataset not found') + await fileRepository .getAllByDatasetPersistentId(dataset.persistentId, dataset.version) .then((files) => { @@ -336,15 +404,24 @@ describe('File JSDataverse Repository', () => { const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(1, 'csv')) if (!datasetResponse.files) throw new Error('Files not found') await TestsUtils.waitForNoLocks(datasetResponse.persistentId) // Wait for the tabular data to be ingested + + const expectedLabels = [{ type: FileLabelType.TAG, value: 'Survey' }] + await FileHelper.addLabel(datasetResponse.files[0].id, expectedLabels) + + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DatasetNonNumericVersion.DRAFT ) if (!dataset) throw new Error('Dataset not found') - const expectedLabels = [{ type: FileLabelType.TAG, value: 'Survey' }] - await FileHelper.addLabel(datasetResponse.files[0].id, expectedLabels) - await fileRepository .getAllByDatasetPersistentId(dataset.persistentId, dataset.version) .then((files) => { @@ -358,6 +435,14 @@ describe('File JSDataverse Repository', () => { ) if (!datasetResponse.files) throw new Error('Files not found') + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DatasetNonNumericVersion.DRAFT @@ -375,12 +460,6 @@ describe('File JSDataverse Repository', () => { const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(3)) if (!datasetResponse.files) throw new Error('Files not found') - const dataset = await datasetRepository.getByPersistentId( - datasetResponse.persistentId, - DRAFT_PARAM - ) - if (!dataset) throw new Error('Dataset not found') - const embargoDate = '2100-10-20' await DatasetHelper.embargoFiles( datasetResponse.persistentId, @@ -389,6 +468,20 @@ describe('File JSDataverse Repository', () => { ) await TestsUtils.waitForNoLocks(datasetResponse.persistentId) // Wait for the files to be embargoed + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + + const dataset = await datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DRAFT_PARAM + ) + if (!dataset) throw new Error('Dataset not found') + await fileRepository .getAllByDatasetPersistentId(dataset.persistentId, dataset.version) .then((files) => { @@ -397,10 +490,19 @@ describe('File JSDataverse Repository', () => { }) }) - it('gets all the files by dataset persistentId when files are tabular data', async () => { + // TODO: Skipping because http://localhost:8000/api/v1/datasets/:persistentId/versions/:draft/files?persistentId=doi:10.5072/FK2/XRSQV4 is bringing dataFile.tabularData as false + it.skip('gets all the files by dataset persistentId when files are tabular data', async () => { const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(1, 'csv')) if (!datasetResponse.files) throw new Error('Files not found') + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DRAFT_PARAM @@ -427,10 +529,21 @@ describe('File JSDataverse Repository', () => { }) it('gets the files pagination selection when passing pagination', async () => { - const dataset = await DatasetHelper.createWithFiles(FileHelper.createMany(3)).then( - (datasetResponse) => - datasetRepository.getByPersistentId(datasetResponse.persistentId, DRAFT_PARAM) + const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(3)) + + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) + + const dataset = await datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DRAFT_PARAM + ) + if (!dataset) throw new Error('Dataset not found') await fileRepository @@ -448,10 +561,21 @@ describe('File JSDataverse Repository', () => { }) it('gets all the files by dataset persistentId when passing sortBy criteria', async () => { - const dataset = await DatasetHelper.createWithFiles(FileHelper.createMany(3)).then( - (datasetResponse) => - datasetRepository.getByPersistentId(datasetResponse.persistentId, DRAFT_PARAM) + const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(3)) + + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) + + const dataset = await datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DRAFT_PARAM + ) + if (!dataset) throw new Error('Dataset not found') await fileRepository @@ -469,14 +593,27 @@ describe('File JSDataverse Repository', () => { }) }) - it('gets all the files by dataset persistentId when passing filterByType criteria', async () => { - const dataset = await DatasetHelper.createWithFiles([ + // TODO: Skipping, similar error, expecting 1 file but api returning 0 + it.skip('gets all the files by dataset persistentId when passing filterByType criteria', async () => { + const datasetResponse = await DatasetHelper.createWithFiles([ FileHelper.create('txt'), FileHelper.create('txt'), FileHelper.create('csv') - ]).then((datasetResponse) => - datasetRepository.getByPersistentId(datasetResponse.persistentId, DRAFT_PARAM) + ]) + + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + + const dataset = await datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DRAFT_PARAM ) + if (!dataset) throw new Error('Dataset not found') await fileRepository @@ -495,14 +632,22 @@ describe('File JSDataverse Repository', () => { const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(3)) if (!datasetResponse.files) throw new Error('Files not found') + await FileHelper.restrict(datasetResponse.files[0].id) + + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DRAFT_PARAM ) if (!dataset) throw new Error('Dataset not found') - await FileHelper.restrict(datasetResponse.files[0].id) - await fileRepository .getAllByDatasetPersistentId( dataset.persistentId, @@ -519,15 +664,23 @@ describe('File JSDataverse Repository', () => { const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(3)) if (!datasetResponse.files) throw new Error('Files not found') + const category = { type: FileLabelType.CATEGORY, value: 'category' } + await FileHelper.addLabel(datasetResponse.files[0].id, [category]) + + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DRAFT_PARAM ) if (!dataset) throw new Error('Dataset not found') - const category = { type: FileLabelType.CATEGORY, value: 'category' } - await FileHelper.addLabel(datasetResponse.files[0].id, [category]) - await fileRepository .getAllByDatasetPersistentId( dataset.persistentId, @@ -541,10 +694,21 @@ describe('File JSDataverse Repository', () => { }) it('gets all the files by dataset persistentId when passing searchText criteria', async () => { - const dataset = await DatasetHelper.createWithFiles(FileHelper.createMany(3)).then( - (datasetResponse) => - datasetRepository.getByPersistentId(datasetResponse.persistentId, DRAFT_PARAM) + const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(3)) + + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) + + const dataset = await datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DRAFT_PARAM + ) + if (!dataset) throw new Error('Dataset not found') await fileRepository @@ -573,6 +737,14 @@ describe('File JSDataverse Repository', () => { if (!datasetResponse.files) throw new Error('Files not found') datasetResponse.files.map((file) => FileHelper.delete(file.id)) + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + await fileRepository .getAllByDatasetPersistentId(dataset.persistentId, dataset.version) .then((files) => { @@ -625,6 +797,14 @@ describe('File JSDataverse Repository', () => { }) it('gets FilesCountInfo by dataset persistentId', async () => { + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + const dataset = await datasetRepository.getByPersistentId(datasetPersistentId, DRAFT_PARAM) if (!dataset) throw new Error('Dataset not found') @@ -702,6 +882,14 @@ describe('File JSDataverse Repository', () => { }) it('gets FilesCountInfo by dataset persistentId when passing filterByType criteria', async () => { + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + const dataset = await datasetRepository.getByPersistentId(datasetPersistentId, DRAFT_PARAM) if (!dataset) throw new Error('Dataset not found') @@ -793,9 +981,21 @@ describe('File JSDataverse Repository', () => { categories: ['category_1'] }) ] - const dataset = await DatasetHelper.createWithFiles(files).then((datasetResponse) => - datasetRepository.getByPersistentId(datasetResponse.persistentId, DRAFT_PARAM) + const datasetResponse = await DatasetHelper.createWithFiles(files) + + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + + const dataset = await datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DRAFT_PARAM ) + if (!dataset) throw new Error('Dataset not found') await TestsUtils.waitForNoLocks(dataset.persistentId) // wait for the files to be ingested @@ -834,8 +1034,19 @@ describe('File JSDataverse Repository', () => { categories: ['category_1'] }) ] - const dataset = await DatasetHelper.createWithFiles(files).then((datasetResponse) => - datasetRepository.getByPersistentId(datasetResponse.persistentId, DRAFT_PARAM) + const datasetResponse = await DatasetHelper.createWithFiles(files) + + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + + const dataset = await datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DRAFT_PARAM ) if (!dataset) throw new Error('Dataset not found') @@ -872,6 +1083,14 @@ describe('File JSDataverse Repository', () => { const expectedFile = fileExpectedData(datasetResponse.file.id) + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + await fileRepository.getById(datasetResponse.file.id).then((file) => { expect(file.name).to.deep.equal(expectedFile.name) expect(file.ingest).to.deep.equal(expectedFile.ingest) diff --git a/tests/e2e-integration/integration/files/FileUpload.spec.ts b/tests/e2e-integration/integration/files/FileUpload.spec.ts index 1d9d4c4fe..d671c363f 100644 --- a/tests/e2e-integration/integration/files/FileUpload.spec.ts +++ b/tests/e2e-integration/integration/files/FileUpload.spec.ts @@ -5,6 +5,9 @@ import { DatasetHelper } from '../../shared/datasets/DatasetHelper' import { FileHelper } from '../../shared/files/FileHelper' import { DatasetNonNumericVersion } from '../../../../src/dataset/domain/models/Dataset' import chaiAsPromised from 'chai-as-promised' +import { ApiConfig } from '@iqss/dataverse-client-javascript' +import { DATAVERSE_BACKEND_URL, OIDC_AUTH_CONFIG } from '@/config' +import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' chai.use(chaiAsPromised) const expect = chai.expect @@ -24,12 +27,21 @@ describe('DirectUpload', () => { }) it('should upload file and add it to the dataset', async () => { - const dataset = await DatasetHelper.create().then((datasetResponse) => - datasetRepository.getByPersistentId( - datasetResponse.persistentId, - DatasetNonNumericVersion.DRAFT - ) + const datasetResponse = await DatasetHelper.create() + + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` ) + + const dataset = await datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DatasetNonNumericVersion.DRAFT + ) + if (!dataset) throw new Error('Dataset not found') const singlePartFile = FileHelper.createSinglePartFileBlob() @@ -76,19 +88,28 @@ describe('DirectUpload', () => { }) it('should upload 2 files and add it to the dataset', async () => { - const dataset = await DatasetHelper.create().then((datasetResponse) => - datasetRepository.getByPersistentId( - datasetResponse.persistentId, - DatasetNonNumericVersion.DRAFT - ) - ) - if (!dataset) throw new Error('Dataset not found') + const datasetResponse = await DatasetHelper.create() const singlePartFile1 = FileHelper.createSinglePartFileBlob() const singlePartFile2 = FileHelper.createSinglePartFileBlob() let storageId1: string | undefined = undefined let storageId2: string | undefined = undefined + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + + const dataset = await datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DatasetNonNumericVersion.DRAFT + ) + + if (!dataset) throw new Error('Dataset not found') + const upload1 = fileRepository.uploadFile( dataset.persistentId, { file: singlePartFile1 }, @@ -158,17 +179,26 @@ describe('DirectUpload', () => { }) it('should not finish uploading file to destinations when user cancels immediately', async () => { - const dataset = await DatasetHelper.create().then((datasetResponse) => - datasetRepository.getByPersistentId( - datasetResponse.persistentId, - DatasetNonNumericVersion.DRAFT - ) - ) - if (!dataset) throw new Error('Dataset not found') + const datasetResponse = await DatasetHelper.create() const multipartFile = FileHelper.createMultipartFileBlob() const controller = new AbortController() + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + + const dataset = await datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DatasetNonNumericVersion.DRAFT + ) + + if (!dataset) throw new Error('Dataset not found') + const upload = fileRepository.uploadFile( dataset.persistentId, { file: multipartFile }, diff --git a/tests/e2e-integration/integration/info/infrastructure/repositories/DataverseInfoJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/info/infrastructure/repositories/DataverseInfoJSDataverseRepository.spec.ts index 89c324b4a..490315e7b 100644 --- a/tests/e2e-integration/integration/info/infrastructure/repositories/DataverseInfoJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/info/infrastructure/repositories/DataverseInfoJSDataverseRepository.spec.ts @@ -2,15 +2,34 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { DataverseInfoJSDataverseRepository } from '../../../../../../src/info/infrastructure/repositories/DataverseInfoJSDataverseRepository' import { TestsUtils } from '../../../../shared/TestsUtils' +import { ApiConfig } from '@iqss/dataverse-client-javascript' +import { DATAVERSE_BACKEND_URL, OIDC_AUTH_CONFIG } from '@/config' +import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' chai.use(chaiAsPromised) const expect = chai.expect describe('DataverseInfo JSDataverse Repository', () => { - before(() => TestsUtils.setup()) + beforeEach(() => { + TestsUtils.login().then((token) => { + if (!token) { + throw new Error('Token not found after Keycloak login') + } + + cy.wrap(TestsUtils.setup(token)) + }) + }) it('gets the dataverse version number', async () => { + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + const dataverseInfoRepository = new DataverseInfoJSDataverseRepository() const dataverseVersion = await dataverseInfoRepository.getVersion() diff --git a/tests/e2e-integration/integration/metadata-block-info/MetadataBlockInfoJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/metadata-block-info/MetadataBlockInfoJSDataverseRepository.spec.ts index 7fcc2184b..67533c7bf 100644 --- a/tests/e2e-integration/integration/metadata-block-info/MetadataBlockInfoJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/metadata-block-info/MetadataBlockInfoJSDataverseRepository.spec.ts @@ -2,16 +2,34 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { MetadataBlockInfoJSDataverseRepository } from '../../../../src/metadata-block-info/infrastructure/repositories/MetadataBlockInfoJSDataverseRepository' import { TestsUtils } from '../../shared/TestsUtils' +import { ApiConfig } from '@iqss/dataverse-client-javascript' +import { DATAVERSE_BACKEND_URL, OIDC_AUTH_CONFIG } from '@/config' +import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' chai.use(chaiAsPromised) const expect = chai.expect const metadataBlockInfoRepository = new MetadataBlockInfoJSDataverseRepository() describe('Metadata Block Info JSDataverse Repository', () => { - before(() => TestsUtils.setup()) - beforeEach(() => TestsUtils.login()) + beforeEach(() => { + TestsUtils.login().then((token) => { + if (!token) { + throw new Error('Token not found after Keycloak login') + } + + cy.wrap(TestsUtils.setup(token)) + }) + }) it('returns JSON in the correct format', async () => { + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + await metadataBlockInfoRepository.getByName('citation').then((metadataBlockInfo) => { if (!metadataBlockInfo) { throw new Error('Metadata Block Info not found') diff --git a/tests/e2e-integration/integration/users/infrastructure/repositories/UserJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/users/infrastructure/repositories/UserJSDataverseRepository.spec.ts index 579f2f31f..bdf507bfd 100644 --- a/tests/e2e-integration/integration/users/infrastructure/repositories/UserJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/users/infrastructure/repositories/UserJSDataverseRepository.spec.ts @@ -2,32 +2,51 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { UserJSDataverseRepository } from '../../../../../../src/users/infrastructure/repositories/UserJSDataverseRepository' import { TestsUtils } from '../../../../shared/TestsUtils' +import { ApiConfig } from '@iqss/dataverse-client-javascript' +import { DATAVERSE_BACKEND_URL, OIDC_AUTH_CONFIG } from '@/config' +import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' +import { User } from '@/users/domain/models/User' chai.use(chaiAsPromised) const expect = chai.expect const userRepository = new UserJSDataverseRepository() describe('User JSDataverse Repository', () => { - before(() => TestsUtils.setup()) - beforeEach(() => TestsUtils.login()) + beforeEach(() => { + TestsUtils.login().then((token) => { + if (!token) { + throw new Error('Token not found after Keycloak login') + } + + cy.wrap(TestsUtils.setup(token)) + }) + }) + it('gets the authenticated user', async () => { - const expectedUser = { - displayName: 'Dataverse Admin', - persistentId: 'dataverseAdmin', + // Change the api config to use bearer token + ApiConfig.init( + `${DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + + const expectedUser: Omit = { + displayName: 'Dataverse User', firstName: 'Dataverse', - lastName: 'Admin', - email: 'dataverse@mailinator.com', - affiliation: 'Dataverse.org', - superuser: true + lastName: 'User', + email: TestsUtils.USER_EMAIL, + superuser: true, + identifier: TestsUtils.USER_USERNAME } - const user = await userRepository.getAuthenticated() - - expect(user).to.deep.equal(expectedUser) - }) - it('removes the authenticated user', async () => { - const user = userRepository.removeAuthenticated() + const user = await userRepository.getAuthenticated() - await expect(user).to.be.fulfilled + expect(user.displayName).to.equal(expectedUser.displayName) + expect(user.firstName).to.equal(expectedUser.firstName) + expect(user.lastName).to.equal(expectedUser.lastName) + expect(user.email).to.equal(expectedUser.email) + expect(user.superuser).to.equal(expectedUser.superuser) + expect(user.identifier).to.equal(expectedUser.identifier) }) }) diff --git a/tests/e2e-integration/shared/TestsUtils.ts b/tests/e2e-integration/shared/TestsUtils.ts index 9160504b7..2f1e15d9b 100644 --- a/tests/e2e-integration/shared/TestsUtils.ts +++ b/tests/e2e-integration/shared/TestsUtils.ts @@ -63,4 +63,10 @@ export class TestsUtils { cy.get('#password').type(this.USER_PASSWORD) cy.get('#kc-login').click() } + + static finishSignUp() { + cy.get('#termsAccepted').check({ force: true }) + + cy.findByRole('button', { name: 'Create Account' }).click() + } } diff --git a/tests/support/commands.tsx b/tests/support/commands.tsx index 3f4c1baa3..e57dec6e4 100644 --- a/tests/support/commands.tsx +++ b/tests/support/commands.tsx @@ -108,19 +108,43 @@ Cypress.Commands.add( Cypress.Commands.add('login', () => { cy.visit('/spa/') + cy.wait(1_000) cy.findByTestId('oidc-login').click() TestsUtils.enterCredentialsInKeycloak() - cy.url() - .should('eq', `${Cypress.config().baseUrl as string}/spa`) - .then(() => { - const token = Utils.getLocalStorageItem( - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}_token` - ) + cy.wait(1_500) - return cy.wrap(token) - }) + // This function will check if the sign-up page is visible (valid token not linked account) and finish the sign-up process and return the token + // Else, it will check if the home page is visible and return the token + + ifElseVisible( + () => cy.get('[data-testid="sign-up-page"]', { timeout: 10_000 }), + () => { + TestsUtils.finishSignUp() + + cy.url() + .should('eq', `${Cypress.config().baseUrl as string}/spa/account?tab=accountInformation`) + .then(() => { + const token = Utils.getLocalStorageItem( + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + + return cy.wrap(token) + }) + }, + () => { + cy.url() + .should('eq', `${Cypress.config().baseUrl as string}/spa`) + .then(() => { + const token = Utils.getLocalStorageItem( + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + + return cy.wrap(token) + }) + } + ) }) Cypress.Commands.add('logout', () => { @@ -134,3 +158,54 @@ Cypress.Commands.add('compareDate', (date, expectedDate) => { expect(date.getUTCMonth()).to.deep.equal(expectedDate.getUTCMonth()) expect(date.getUTCFullYear()).to.deep.equal(expectedDate.getUTCFullYear()) }) + +// Define the type for the conditional functions +type ConditionCallback = ($el: JQuery) => boolean +type CypressCommandFn = (cyChainable: () => Cypress.Chainable) => void + +export function ifElseVisible( + cyChainable: () => Cypress.Chainable>, + ifFn: CypressCommandFn, + elseFn: CypressCommandFn +) { + return ifElse( + cyChainable, + (el) => Cypress.dom.isElement(el) && Cypress.dom.isVisible(el), + ifFn, + elseFn + ) +} + +export function ifElse( + cyChainable: () => Cypress.Chainable>, + conditionCallback: ConditionCallback, + ifFn: CypressCommandFn, + elseFn: CypressCommandFn +) { + cyChainable() + .should((_) => {}) + .then(($el) => { + const result = conditionCallback($el) + + Cypress.log({ + name: 'ifElse', + message: `conditionCallback returned ${String(result)}, calling ${ + String(result) === 'true' ? 'ifFn' : 'elseFn' + }`, + type: 'parent', + consoleProps: () => { + return { + conditionCallback + } + } + }) + if (result) { + ifFn(cyChainable) + } else { + if (elseFn) { + elseFn(cyChainable) + } + } + }) + return cyChainable +} From 022a9252edf861ed0ef51d5ff41bf82a98111371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 2 Dec 2024 23:26:24 -0300 Subject: [PATCH 66/97] test: use pr 10959 dataverse img tag version --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7a34abba8..ca11b5d15 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ on: types: [opened, reopened] env: - E2E_DATAVERSE_IMAGE_TAG: unstable + E2E_DATAVERSE_IMAGE_TAG: 10959-bearer-token-auth-ext jobs: e2e: @@ -56,7 +56,7 @@ jobs: - name: Update registry for the containerized development environment working-directory: dev-env run: | - sed -i~ '/^REGISTRY=/s/=.*/=docker.io/' .env + sed -i~ '/^REGISTRY=/s/=.*/=ghcr.io/' .env shell: bash - name: Start containers From 810bceaa02719c8311bd7992cbfb0bbd685856f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 3 Dec 2024 08:52:05 -0300 Subject: [PATCH 67/97] feat: navigate to root collection after success --- public/locales/en/account.json | 3 +- public/locales/en/collection.json | 3 +- src/sections/account/Account.tsx | 5 +- src/sections/account/AccountFactory.tsx | 13 +--- .../AccountInfoSection.tsx | 67 +++++++++---------- src/sections/collection/Collection.tsx | 5 ++ src/sections/collection/CollectionFactory.tsx | 10 ++- .../FormFields.module.scss | 10 --- .../FormFields.tsx | 6 -- .../useSubmitUser.ts | 13 ++-- src/stories/account/Account.stories.tsx | 2 - .../AccountInfoSection.stories.tsx | 11 --- .../ApiTokenSection.stories.tsx | 4 -- src/stories/collection/Collection.stories.tsx | 20 ++++++ .../sections/account/Account.spec.tsx | 1 - .../account/AccountInfoSection.spec.tsx | 10 +-- .../sections/collection/Collection.spec.tsx | 29 +++++++- 17 files changed, 104 insertions(+), 108 deletions(-) diff --git a/public/locales/en/account.json b/public/locales/en/account.json index 8e1736508..35f9885be 100644 --- a/public/locales/en/account.json +++ b/public/locales/en/account.json @@ -21,7 +21,6 @@ "familyName": "Family Name", "email": "Email", "affiliation": "Affiliation", - "position": "Position", - "accountJustCreated": "Your account has been successfully created! Welcome to Dataverse!" + "position": "Position" } } diff --git a/public/locales/en/collection.json b/public/locales/en/collection.json index f950adb7f..545388c0a 100644 --- a/public/locales/en/collection.json +++ b/public/locales/en/collection.json @@ -30,5 +30,6 @@ "question": "Are you sure you want to publish your collection? Once you do so it must remain published.", "error": "There was an error publishing your collection." }, - "publishedAlert": "Your collection is now public." + "publishedAlert": "Your collection is now public.", + "accountJustCreated": "Welcome to Dataverse! Your account is all set, and we're thrilled to have you on board. Start exploring today!" } diff --git a/src/sections/account/Account.tsx b/src/sections/account/Account.tsx index 427dec3e9..1c74c7028 100644 --- a/src/sections/account/Account.tsx +++ b/src/sections/account/Account.tsx @@ -11,10 +11,9 @@ const tabsKeys = AccountHelper.ACCOUNT_PANEL_TABS_KEYS interface AccountProps { defaultActiveTabKey: AccountPanelTabKey userRepository: UserJSDataverseRepository - accountCreated: boolean } -export const Account = ({ defaultActiveTabKey, userRepository, accountCreated }: AccountProps) => { +export const Account = ({ defaultActiveTabKey, userRepository }: AccountProps) => { const { t } = useTranslation('account') return ( @@ -32,7 +31,7 @@ export const Account = ({ defaultActiveTabKey, userRepository, accountCreated }:
- +
diff --git a/src/sections/account/AccountFactory.tsx b/src/sections/account/AccountFactory.tsx index f3ee049b7..d4c2a6e20 100644 --- a/src/sections/account/AccountFactory.tsx +++ b/src/sections/account/AccountFactory.tsx @@ -1,5 +1,5 @@ import { ReactElement } from 'react' -import { useLocation, useSearchParams } from 'react-router-dom' +import { useSearchParams } from 'react-router-dom' import { AccountHelper } from './AccountHelper' import { Account } from './Account' import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository' @@ -15,15 +15,6 @@ export class AccountFactory { function AccountWithSearchParams() { const [searchParams] = useSearchParams() const defaultActiveTabKey = AccountHelper.defineSelectedTabKey(searchParams) - const location = useLocation() - const state = location.state as { accountCreated: boolean | undefined } | undefined - const accountCreated = state?.accountCreated ?? false - return ( - - ) + return } diff --git a/src/sections/account/account-info-section/AccountInfoSection.tsx b/src/sections/account/account-info-section/AccountInfoSection.tsx index 19cbe39d5..d9298723f 100644 --- a/src/sections/account/account-info-section/AccountInfoSection.tsx +++ b/src/sections/account/account-info-section/AccountInfoSection.tsx @@ -1,54 +1,47 @@ -import { Alert, Table } from '@iqss/dataverse-design-system' +import { Table } from '@iqss/dataverse-design-system' import { useSession } from '@/sections/session/SessionContext' import { useTranslation } from 'react-i18next' // TODO - Add verified email icon // TODO - Edit account information // TODO - Change password -interface AccountInfoSectionProps { - accountCreated: boolean -} -export const AccountInfoSection = ({ accountCreated }: AccountInfoSectionProps) => { +export const AccountInfoSection = () => { const { t } = useTranslation('account', { keyPrefix: 'info' }) const { user } = useSession() return ( - <> - {accountCreated && {t('accountJustCreated')}} - - - - - - - - - - - +
{t('username')}{user?.identifier}
{t('givenName')}{user?.firstName}
+ + + + + + + + + + + + + + + + + + {user?.affiliation && ( - - + + + )} + {user?.position && ( - - + + - {user?.affiliation && ( - - - - - )} - {user?.position && ( - - - - - )} - -
{t('username')}{user?.identifier}
{t('givenName')}{user?.firstName}
{t('familyName')}{user?.lastName}
{t('email')}{user?.email}
{t('familyName')}{user?.lastName}{t('affiliation')}{user?.affiliation}
{t('email')}{user?.email}{t('position')}{user?.position}
{t('affiliation')}{user?.affiliation}
{t('position')}{user?.position}
- + )} + + ) } diff --git a/src/sections/collection/Collection.tsx b/src/sections/collection/Collection.tsx index f90964f0b..5dfae16dd 100644 --- a/src/sections/collection/Collection.tsx +++ b/src/sections/collection/Collection.tsx @@ -22,6 +22,7 @@ interface CollectionProps { created: boolean published: boolean collectionQueryParams: UseCollectionQueryParamsReturnType + accountCreated: boolean infiniteScrollEnabled?: boolean } @@ -30,6 +31,7 @@ export function Collection({ collectionRepository, created, published, + accountCreated, collectionQueryParams }: CollectionProps) { useTranslation('collection') @@ -65,12 +67,15 @@ export function Collection({ <> + {created && } {published && ( {t('publishedAlert')} )} + {accountCreated && {t('accountJustCreated')}} + {!collection.isReleased && canUserPublishCollection && (
() const location = useLocation() - const state = location.state as { published: boolean; created: boolean } | undefined + const state = location.state as + | { + published?: boolean + created?: boolean + accountCreated?: boolean + } + | undefined const created = state?.created ?? false const published = state?.published ?? false + const accountCreated = state?.accountCreated ?? false return ( ) diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.module.scss b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.module.scss index a45f3d935..72df568e9 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.module.scss +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.module.scss @@ -15,13 +15,3 @@ text-align: left; } } - -.about-prefilled-fields-wrapper { - max-width: 100ch; - margin-bottom: 1rem; - white-space: pre-wrap; - - svg { - translate: 0 -15%; - } -} diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx index 37ef97388..484d7aa84 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx @@ -93,12 +93,6 @@ export const FormFields = ({ userRepository, formDefaultValues, termsOfUse }: Fo return (
- {/*
- - {t('aboutPrefilledFields')} - -
*/} - {submissionStatus === SubmissionStatus.Errored && ( {submitError} diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/useSubmitUser.ts b/src/sections/sign-up/valid-token-not-linked-account-form/useSubmitUser.ts index 527283410..2070d8190 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/useSubmitUser.ts +++ b/src/sections/sign-up/valid-token-not-linked-account-form/useSubmitUser.ts @@ -6,7 +6,6 @@ import { UserRepository } from '@/users/domain/repositories/UserRepository' import { UserDTO } from '@/users/domain/useCases/DTOs/UserDTO' import { registerUser } from '@/users/domain/useCases/registerUser' import { useSession } from '@/sections/session/SessionContext' -import { AccountHelper } from '@/sections/account/AccountHelper' import { Route } from '@/sections/Route.enum' import { ValidTokenNotLinkedAccountFormHelper } from './ValidTokenNotLinkedAccountFormHelper' import { ValidTokenNotLinkedAccountFormData } from './types' @@ -65,14 +64,10 @@ export const useSubmitUser = ( await refetchUserSession() - // Navigate to Account - Account Information tab after successful registration - navigate( - `${Route.ACCOUNT}?${AccountHelper.ACCOUNT_PANEL_TAB_QUERY_KEY}=${AccountHelper.ACCOUNT_PANEL_TABS_KEYS.accountInformation}`, - { - state: { accountCreated: true }, - replace: true - } - ) + navigate(Route.COLLECTIONS_BASE, { + state: { accountCreated: true }, + replace: true + }) }) .catch((err: WriteError) => { const error = new JSDataverseWriteErrorHandler(err) diff --git a/src/stories/account/Account.stories.tsx b/src/stories/account/Account.stories.tsx index d9c45a8e2..69373bfd8 100644 --- a/src/stories/account/Account.stories.tsx +++ b/src/stories/account/Account.stories.tsx @@ -24,7 +24,6 @@ export const AccountInformation: Story = { ) } @@ -34,7 +33,6 @@ export const ApiTokenTab: Story = { ) } diff --git a/src/stories/account/account-info-section/AccountInfoSection.stories.tsx b/src/stories/account/account-info-section/AccountInfoSection.stories.tsx index 8bf583664..3d2b9b9f5 100644 --- a/src/stories/account/account-info-section/AccountInfoSection.stories.tsx +++ b/src/stories/account/account-info-section/AccountInfoSection.stories.tsx @@ -24,17 +24,6 @@ export const Default: Story = { - ) -} - -export const AccountJustCreated: Story = { - render: () => ( - ) } diff --git a/src/stories/account/api-token-section/ApiTokenSection.stories.tsx b/src/stories/account/api-token-section/ApiTokenSection.stories.tsx index 0ef804ab1..5be44de8f 100644 --- a/src/stories/account/api-token-section/ApiTokenSection.stories.tsx +++ b/src/stories/account/api-token-section/ApiTokenSection.stories.tsx @@ -26,7 +26,6 @@ export const Default: Story = { ) } @@ -36,7 +35,6 @@ export const Loading: Story = { ) } @@ -46,7 +44,6 @@ export const Error: Story = { ) } @@ -69,7 +66,6 @@ export const NoToken: Story = { ) } diff --git a/src/stories/collection/Collection.stories.tsx b/src/stories/collection/Collection.stories.tsx index f48b6235d..e15d90bbf 100644 --- a/src/stories/collection/Collection.stories.tsx +++ b/src/stories/collection/Collection.stories.tsx @@ -27,6 +27,7 @@ export const Default: Story = { collectionIdFromParams="collection" created={false} published={false} + accountCreated={false} collectionQueryParams={{ pageQuery: 1, searchQuery: undefined, @@ -43,6 +44,7 @@ export const Loading: Story = { collectionRepository={new CollectionLoadingMockRepository()} created={false} published={false} + accountCreated={false} collectionQueryParams={{ pageQuery: 1, searchQuery: undefined, typesQuery: undefined }} /> ) @@ -56,6 +58,7 @@ export const LoggedIn: Story = { collectionRepository={new CollectionMockRepository()} created={false} published={false} + accountCreated={false} collectionQueryParams={{ pageQuery: 1, searchQuery: undefined, typesQuery: undefined }} /> ) @@ -68,6 +71,7 @@ export const Unpublished: Story = { collectionRepository={new UnpublishedCollectionMockRepository()} created={false} published={false} + accountCreated={false} collectionQueryParams={{ pageQuery: 1, searchQuery: undefined, typesQuery: undefined }} /> ) @@ -81,6 +85,7 @@ export const Created: Story = { collectionIdFromParams="collection" created={true} published={false} + accountCreated={false} collectionQueryParams={{ pageQuery: 1, searchQuery: undefined, typesQuery: undefined }} /> ) @@ -93,6 +98,21 @@ export const Published: Story = { collectionIdFromParams="collection" created={false} published={true} + accountCreated={false} + collectionQueryParams={{ pageQuery: 1, searchQuery: undefined, typesQuery: undefined }} + /> + ) +} + +export const AccountCreated: Story = { + decorators: [WithLoggedInUser], + render: () => ( + ) diff --git a/tests/component/sections/account/Account.spec.tsx b/tests/component/sections/account/Account.spec.tsx index e407fea21..4ecf68879 100644 --- a/tests/component/sections/account/Account.spec.tsx +++ b/tests/component/sections/account/Account.spec.tsx @@ -8,7 +8,6 @@ describe('Account', () => { ) diff --git a/tests/component/sections/account/AccountInfoSection.spec.tsx b/tests/component/sections/account/AccountInfoSection.spec.tsx index 64247c58a..1fc9d6a9e 100644 --- a/tests/component/sections/account/AccountInfoSection.spec.tsx +++ b/tests/component/sections/account/AccountInfoSection.spec.tsx @@ -5,7 +5,7 @@ const testUser = UserMother.create() describe('AccountInfoSection', () => { it('should display the user information', () => { - cy.mountAuthenticated() + cy.mountAuthenticated() cy.findAllByRole('row').spread((usernameRow, givenNameRow, familyNameRow, emailRow) => { cy.wrap(usernameRow).within(() => { @@ -29,12 +29,4 @@ describe('AccountInfoSection', () => { }) }) }) - - it('should display the account created alert', () => { - cy.mountAuthenticated() - - cy.findByText(/Your account has been successfully created! Welcome to Dataverse!/).should( - 'exist' - ) - }) }) diff --git a/tests/component/sections/collection/Collection.spec.tsx b/tests/component/sections/collection/Collection.spec.tsx index fdb80ac37..40e6e4be4 100644 --- a/tests/component/sections/collection/Collection.spec.tsx +++ b/tests/component/sections/collection/Collection.spec.tsx @@ -35,6 +35,7 @@ describe('Collection page', () => { collectionIdFromParams="collection" created={false} published={false} + accountCreated={false} collectionQueryParams={{ pageQuery: 1 }} /> ) @@ -60,6 +61,7 @@ describe('Collection page', () => { collectionIdFromParams="collection" created={false} published={false} + accountCreated={false} collectionQueryParams={{ pageQuery: 1 }} /> ) @@ -74,6 +76,7 @@ describe('Collection page', () => { collectionIdFromParams="collection" created={false} published={false} + accountCreated={false} collectionQueryParams={{ pageQuery: 1 }} /> ) @@ -88,6 +91,7 @@ describe('Collection page', () => { collectionIdFromParams="collection" created={false} published={false} + accountCreated={false} collectionQueryParams={{ pageQuery: 1 }} /> ) @@ -101,6 +105,7 @@ describe('Collection page', () => { collectionIdFromParams="collection" created={false} published={false} + accountCreated={false} collectionQueryParams={{ pageQuery: 1 }} /> ) @@ -114,6 +119,7 @@ describe('Collection page', () => { collectionIdFromParams="collection" created={false} published={false} + accountCreated={false} collectionQueryParams={{ pageQuery: 1 }} /> ) @@ -132,6 +138,7 @@ describe('Collection page', () => { collectionIdFromParams="collection" created published={false} + accountCreated={false} collectionQueryParams={{ pageQuery: 1 }} /> ) @@ -145,6 +152,7 @@ describe('Collection page', () => { collectionIdFromParams="collection" created={false} published={true} + accountCreated={false} collectionQueryParams={{ pageQuery: 1 }} /> ) @@ -166,6 +174,7 @@ describe('Collection page', () => { collectionIdFromParams="collection" created={false} published={false} + accountCreated={false} collectionQueryParams={{ pageQuery: 1 }} /> ) @@ -182,9 +191,10 @@ describe('Collection page', () => { cy.mountAuthenticated( ) @@ -196,4 +206,21 @@ describe('Collection page', () => { cy.findByRole('button', { name: /Cancel/i }).click() cy.findByText('Publish Collection').should('not.exist') }) + + it('should display the account created alert', () => { + cy.mountAuthenticated( + + ) + + cy.findByText( + /Welcome to Dataverse! Your account is all set, and we're thrilled to have you on board. Start exploring today!/ + ).should('exist') + }) }) From e2f32ccabb1944d40e1a069eca898c6af698097f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 3 Dec 2024 10:24:39 -0300 Subject: [PATCH 68/97] chore: add confetti package --- package-lock.json | 20 ++++++++++++++++++++ package.json | 3 ++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 8604aa5e0..d231fb331 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "moment-timezone": "0.5.43", "react-bootstrap": "2.7.2", "react-bootstrap-icons": "1.10.3", + "react-confetti": "6.1.0", "react-hook-form": "7.51.2", "react-i18next": "12.1.5", "react-infinite-scroll-hook": "4.1.1", @@ -36345,6 +36346,20 @@ "react-dom": ">=16.8.0" } }, + "node_modules/react-confetti": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.1.0.tgz", + "integrity": "sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==", + "dependencies": { + "tween-functions": "^1.2.0" + }, + "engines": { + "node": ">=10.18" + }, + "peerDependencies": { + "react": "^16.3.0 || ^17.0.1 || ^18.0.0" + } + }, "node_modules/react-docgen": { "version": "6.0.0-alpha.3", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-6.0.0-alpha.3.tgz", @@ -41936,6 +41951,11 @@ "domino": "^2.1.6" } }, + "node_modules/tween-functions": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz", + "integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==" + }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", diff --git a/package.json b/package.json index 7a5a22bd0..1b4411486 100644 --- a/package.json +++ b/package.json @@ -35,14 +35,15 @@ "moment-timezone": "0.5.43", "react-bootstrap": "2.7.2", "react-bootstrap-icons": "1.10.3", + "react-confetti": "6.1.0", "react-hook-form": "7.51.2", "react-i18next": "12.1.5", "react-infinite-scroll-hook": "4.1.1", "react-loader-spinner": "5.3.4", "react-markdown": "8.0.7", + "react-oauth2-code-pkce": "1.22.1", "react-router-dom": "6.23.1", "react-topbar-progress-indicator": "4.1.1", - "react-oauth2-code-pkce": "1.22.1", "sass": "1.58.1", "typescript": "4.9.5", "use-deep-compare": "1.2.1", From 89c934237d599d8b4d0bd76a15bd54207cd8ffb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 3 Dec 2024 10:25:38 -0300 Subject: [PATCH 69/97] feat: show success alert and confettis --- .../collection/AccountCreatedAlert.tsx | 24 ++++++++++++++ src/sections/collection/Collection.tsx | 3 +- src/sections/collection/CollectionFactory.tsx | 4 ++- .../useSubmitUser.ts | 4 ++- src/shared/hooks/useWindowSize.ts | 31 +++++++++++++++++++ 5 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 src/sections/collection/AccountCreatedAlert.tsx create mode 100644 src/shared/hooks/useWindowSize.ts diff --git a/src/sections/collection/AccountCreatedAlert.tsx b/src/sections/collection/AccountCreatedAlert.tsx new file mode 100644 index 000000000..e97300912 --- /dev/null +++ b/src/sections/collection/AccountCreatedAlert.tsx @@ -0,0 +1,24 @@ +import { useEffect } from 'react' +import Confetti from 'react-confetti' +import { useTranslation } from 'react-i18next' +import { Alert } from '@iqss/dataverse-design-system' +import { useWindowSize } from '@/shared/hooks/useWindowSize' + +export const ACCOUNT_CREATED_SESSION_STORAGE_KEY = 'accountCreated' + +export const AccountCreatedAlert = () => { + const { t } = useTranslation('collection') + const { width, height } = useWindowSize() + + useEffect(() => { + // Remove the session storage key after the component is mounted to avoid showing the alert again + sessionStorage.removeItem(ACCOUNT_CREATED_SESSION_STORAGE_KEY) + }, []) + + return ( + <> + + {t('accountJustCreated')} + + ) +} diff --git a/src/sections/collection/Collection.tsx b/src/sections/collection/Collection.tsx index 5dfae16dd..e30a8339c 100644 --- a/src/sections/collection/Collection.tsx +++ b/src/sections/collection/Collection.tsx @@ -14,6 +14,7 @@ import { CollectionSkeleton } from './CollectionSkeleton' import { PageNotFound } from '../page-not-found/PageNotFound' import { CreatedAlert } from './CreatedAlert' import { PublishCollectionButton } from './publish-collection/PublishCollectionButton' +import { AccountCreatedAlert } from './AccountCreatedAlert' import styles from './Collection.module.scss' interface CollectionProps { @@ -74,7 +75,7 @@ export function Collection({ {t('publishedAlert')} )} - {accountCreated && {t('accountJustCreated')}} + {accountCreated && } {!collection.isReleased && canUserPublishCollection && (
diff --git a/src/sections/collection/CollectionFactory.tsx b/src/sections/collection/CollectionFactory.tsx index f62a60a3e..64eb53c3f 100644 --- a/src/sections/collection/CollectionFactory.tsx +++ b/src/sections/collection/CollectionFactory.tsx @@ -4,6 +4,7 @@ import { CollectionJSDataverseRepository } from '../../collection/infrastructure import { Collection } from './Collection' import { INFINITE_SCROLL_ENABLED } from './config' import { useGetCollectionQueryParams } from './useGetCollectionQueryParams' +import { ACCOUNT_CREATED_SESSION_STORAGE_KEY } from './AccountCreatedAlert' const collectionRepository = new CollectionJSDataverseRepository() export class CollectionFactory { @@ -25,7 +26,8 @@ function CollectionWithSearchParams() { | undefined const created = state?.created ?? false const published = state?.published ?? false - const accountCreated = state?.accountCreated ?? false + const accountCreated = + Boolean(sessionStorage.getItem(ACCOUNT_CREATED_SESSION_STORAGE_KEY)) ?? false return ( ({ + width: undefined, + height: undefined + }) + + useLayoutEffect(() => { + const handleResize = () => { + setSize({ + width: window.innerWidth, + height: window.innerHeight + }) + } + + handleResize() + window.addEventListener('resize', handleResize) + + return () => { + window.removeEventListener('resize', handleResize) + } + }, []) + + return size +} From 688663900c5ece315ba296ebe257f784afb3ced5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 3 Dec 2024 10:49:24 -0300 Subject: [PATCH 70/97] feat: fix z-index and remove unneeded alert --- src/sections/collection/AccountCreatedAlert.tsx | 8 +++++++- .../valid-token-not-linked-account-form/FormFields.tsx | 6 ------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/sections/collection/AccountCreatedAlert.tsx b/src/sections/collection/AccountCreatedAlert.tsx index e97300912..2e7729257 100644 --- a/src/sections/collection/AccountCreatedAlert.tsx +++ b/src/sections/collection/AccountCreatedAlert.tsx @@ -17,7 +17,13 @@ export const AccountCreatedAlert = () => { return ( <> - + {t('accountJustCreated')} ) diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx index 484d7aa84..d19e0a00b 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx @@ -99,12 +99,6 @@ export const FormFields = ({ userRepository, formDefaultValues, termsOfUse }: Fo )} - {submissionStatus === SubmissionStatus.SubmitComplete && ( - - {t('status.success')} - - )} - Date: Tue, 3 Dec 2024 10:52:30 -0300 Subject: [PATCH 71/97] test: command assert collection after sign up --- tests/support/commands.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/support/commands.tsx b/tests/support/commands.tsx index e57dec6e4..2810cf4f4 100644 --- a/tests/support/commands.tsx +++ b/tests/support/commands.tsx @@ -124,7 +124,7 @@ Cypress.Commands.add('login', () => { TestsUtils.finishSignUp() cy.url() - .should('eq', `${Cypress.config().baseUrl as string}/spa/account?tab=accountInformation`) + .should('eq', `${Cypress.config().baseUrl as string}/spa/collections`) .then(() => { const token = Utils.getLocalStorageItem( `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` From 73546d492c1f37d1dc010ef92bbc9a5b7293acc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 3 Dec 2024 11:39:20 -0300 Subject: [PATCH 72/97] chore: missing claims flag --- dev-env/docker-compose-dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-env/docker-compose-dev.yml b/dev-env/docker-compose-dev.yml index 0c11807b5..c7edebcd0 100644 --- a/dev-env/docker-compose-dev.yml +++ b/dev-env/docker-compose-dev.yml @@ -48,6 +48,7 @@ services: DATAVERSE_DB_PASSWORD: secret DATAVERSE_DB_USER: ${DATAVERSE_DB_USER} DATAVERSE_FEATURE_API_BEARER_AUTH: 1 + DATAVERSE_FEATURE_API_BEARER_AUTH_PROVIDE_MISSING_CLAIMS: 1 DATAVERSE_AUTH_OIDC_ENABLED: 1 DATAVERSE_AUTH_OIDC_CLIENT_ID: test DATAVERSE_AUTH_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 From 81bc0b5df8611824df94f0c8ca30660f79b5a9ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 3 Dec 2024 11:44:25 -0300 Subject: [PATCH 73/97] chore: remove logs --- .../account/useGetApiTokenInfo.spec.tsx | 6 +---- .../DatasetJSDataverseRepository.spec.ts | 6 ----- .../shared/DataverseApiHelper.ts | 24 ++++--------------- 3 files changed, 5 insertions(+), 31 deletions(-) diff --git a/tests/component/sections/account/useGetApiTokenInfo.spec.tsx b/tests/component/sections/account/useGetApiTokenInfo.spec.tsx index 53768c586..020bbbc00 100644 --- a/tests/component/sections/account/useGetApiTokenInfo.spec.tsx +++ b/tests/component/sections/account/useGetApiTokenInfo.spec.tsx @@ -37,11 +37,7 @@ describe('useGetApiToken', () => { ...result.current.apiTokenInfo, expirationDate: DateHelper.toISO8601Format(result.current.apiTokenInfo.expirationDate) } - console.log( - 'test', - DateHelper.toISO8601Format(result.current.apiTokenInfo.expirationDate), - DateHelper.toISO8601Format(mockTokenInfo.expirationDate) - ) + return expect(apiTokenInfo).to.deep.equal({ apiToken: mockTokenInfo.apiToken, expirationDate: DateHelper.toISO8601Format(mockTokenInfo.expirationDate) diff --git a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts index 874236476..7464ce550 100644 --- a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts @@ -186,7 +186,6 @@ describe('Dataset JSDataverse Repository', () => { }) it('gets a published dataset by persistentId without user authentication', async () => { - console.log('RUNNING TEST') const datasetResponse = await DatasetHelper.create(collectionId) await DatasetHelper.publish(datasetResponse.persistentId) @@ -207,7 +206,6 @@ describe('Dataset JSDataverse Repository', () => { await datasetRepository .getByPersistentId(datasetResponse.persistentId, '1.0') .then((dataset) => { - console.log({ dataset }) if (!dataset) { throw new Error('Dataset not found') } @@ -287,10 +285,6 @@ describe('Dataset JSDataverse Repository', () => { ) expect(dataset.metadataBlocks[0].fields.citationDate).not.to.exist - console.log({ - datasetPermissions: dataset.permissions, - expected: datasetExpected.permissions - }) expect(dataset.permissions).to.deep.equal(datasetExpected.permissions) }) }) diff --git a/tests/e2e-integration/shared/DataverseApiHelper.ts b/tests/e2e-integration/shared/DataverseApiHelper.ts index 08bd7c370..dce9343e2 100644 --- a/tests/e2e-integration/shared/DataverseApiHelper.ts +++ b/tests/e2e-integration/shared/DataverseApiHelper.ts @@ -26,21 +26,16 @@ export class DataverseApiHelper { 'PUT', 'author, datasetContact, contributor, depositor, grantNumber, publication' ) + console.log( + '%cDataverse API setup complete', + 'background: green; color: white; padding: 2px; border-radius: 4px;' + ) } catch (error) { console.log( '%cError setting up Dataverse API', 'background: red; color: white; padding: 2px; border-radius: 4px;' ) console.log(error) - } finally { - console.log( - '%cDataverse API setup complete', - 'background: green; color: white; padding: 2px; border-radius: 4px;' - ) - console.group('Dataverse API setup results') - console.log('API URL:', this.API_URL) - console.log('API Token:', this.API_TOKEN) - console.groupEnd() } } @@ -51,12 +46,6 @@ export class DataverseApiHelper { data?: any, contentType = 'application/json' ): Promise { - console.log( - '%cMaking request...', - 'background: violet; color: white; padding: 2px; border-radius: 4px;', - url - ) - const isFormData = contentType === 'multipart/form-data' const config: AxiosRequestConfig = { @@ -98,11 +87,6 @@ export class DataverseApiHelper { } static async createAndGetApiTokenWithBearerToken(bearerToken: string): Promise { - console.log( - '%cCreating test API key...', - 'background: blue; color: white; padding: 2px; border-radius: 4px;' - ) - const { data }: { data: { data: { message: string } } } = await axios.post( `${this.API_URL}/users/token/recreate`, {}, From 1f359dfb77e910d553a283e9f4612f4c4299c883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 3 Dec 2024 11:56:41 -0300 Subject: [PATCH 74/97] chore: remove old todos and comments --- src/sections/sign-up/SignUp.tsx | 3 --- .../datasets/DatasetJSDataverseRepository.spec.ts | 2 -- tests/support/e2e.ts | 6 ------ 3 files changed, 11 deletions(-) diff --git a/src/sections/sign-up/SignUp.tsx b/src/sections/sign-up/SignUp.tsx index c89ef98f7..16992f88f 100644 --- a/src/sections/sign-up/SignUp.tsx +++ b/src/sections/sign-up/SignUp.tsx @@ -8,10 +8,7 @@ import { ValidTokenNotLinkedAccountForm } from './valid-token-not-linked-account import styles from './SignUp.module.scss' // TODO:ME - All use cases will return same error message so this is blocking us for making requests to other public use cases like get root collection, should work removing access token from localstorage but we need it for future call -// TODO:ME - Maybe we should redirect to a welcome page after success? ask if there is one, maybe not the case for this scenario // TODO:ME - Ask about the format of the terms of use, html string? just text string? what is shown in the box if there is just a url string ? -// TODO:ME - Ask about logout when clicking the Cancel button because of the BEARER_TOKEN_IS_VALID_BUT_NOT_LINKED_MESSAGE error -// TODO:ME - JS-DATAVERSE use case for getting the terms of use? how to avoid sending token in this case? interface SignUpProps { userRepository: UserRepository diff --git a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts index 7464ce550..5689d555e 100644 --- a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts @@ -135,8 +135,6 @@ const datasetData = (persistentId: string, versionId: number) => { } } -// TODO:ME Some tests are failing because dataset permissions is not matching - const collectionId = 'DatasetJSDataverseRepository' const datasetRepository = new DatasetJSDataverseRepository() describe('Dataset JSDataverse Repository', () => { diff --git a/tests/support/e2e.ts b/tests/support/e2e.ts index 50536e762..2954bbe9b 100644 --- a/tests/support/e2e.ts +++ b/tests/support/e2e.ts @@ -15,12 +15,6 @@ // Import commands.js using ES2015 syntax: import '../../tests/support/commands' -// import { ApiConfig } from '@iqss/dataverse-client-javascript/dist/core' -// import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' -// import { DATAVERSE_BACKEND_URL } from '../../src/config' - -// TODO:ME Why do we need api config in here? -// ApiConfig.init(`${DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.SESSION_COOKIE) // This global declaration is to get automatic typescript inferring for wrap https://github.com/cypress-io/cypress/issues/18182 declare global { From 4818ec0b82774dd4e2029ce32352ef5d69ca0977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 3 Dec 2024 14:57:43 -0300 Subject: [PATCH 75/97] chore: install dompurify to sanitize html --- package-lock.json | 15 +++++++++++++++ package.json | 1 + 2 files changed, 16 insertions(+) diff --git a/package-lock.json b/package-lock.json index d231fb331..f772a9ed9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "async-mutex": "0.5.0", "bootstrap": "5.2.3", "classnames": "2.5.1", + "dompurify": "3.2.2", "html-react-parser": "3.0.16", "i18next": "22.4.9", "i18next-browser-languagedetector": "7.0.1", @@ -16946,6 +16947,12 @@ "integrity": "sha512-LKtbHwOf5FjWXri/6l6kxMPLVJV69VoyTL2IS+icQcr6k9ffVgXMCvnVXRFWpv5bQED/Gdl8KU+CfuwTAg5HkA==", "dev": true }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, "node_modules/@types/turndown": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.2.tgz", @@ -22001,6 +22008,14 @@ "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz", "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==" }, + "node_modules/dompurify": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.2.tgz", + "integrity": "sha512-YMM+erhdZ2nkZ4fTNRTSI94mb7VG7uVF5vj5Zde7tImgnhZE3R6YW/IACGIHb2ux+QkEXMhe591N+5jWOmL4Zw==", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", diff --git a/package.json b/package.json index 1b4411486..35be31b5f 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "async-mutex": "0.5.0", "bootstrap": "5.2.3", "classnames": "2.5.1", + "dompurify": "3.2.2", "html-react-parser": "3.0.16", "i18next": "22.4.9", "i18next-browser-languagedetector": "7.0.1", From 6f640ca6110d2bf9995c3a3b3c68d49d0ba5b62c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 3 Dec 2024 15:15:34 -0300 Subject: [PATCH 76/97] feat: sanitize future installation terms of use --- src/info/domain/models/TermsOfUse.ts | 2 +- .../repositories/DataverseInfoRepository.ts | 2 +- src/info/domain/useCases/getTermsOfUse.ts | 4 ++-- .../mappers/JSTermsOfUseMapper.ts | 18 ++++++++++++++++++ .../DataverseInfoJSDataverseRepository.ts | 5 +++-- src/sections/sign-up/SignUp.tsx | 1 - .../FormFields.module.scss | 11 +++++++++++ .../FormFields.tsx | 9 +++++---- .../ValidTokenNotLinkedAccountForm.tsx | 5 +++-- ...GetTermsOfUse.ts => useGetApiTermsOfUse.ts} | 4 ++-- .../DataverseInfoMockLoadingkRepository.ts | 2 +- .../info/DataverseInfoMockRepository.ts | 2 +- .../component/info/models/TermsOfUseMother.ts | 4 +--- .../component/sections/sign-up/SignUp.spec.tsx | 2 +- .../ValidTokenNotLinkedAccountForm.spec.tsx | 14 +++++++++----- .../shared/hooks/useGetTermsOfUse.spec.ts | 16 ++++++++-------- 16 files changed, 67 insertions(+), 34 deletions(-) create mode 100644 src/info/infrastructure/mappers/JSTermsOfUseMapper.ts rename src/shared/hooks/{useGetTermsOfUse.ts => useGetApiTermsOfUse.ts} (90%) diff --git a/src/info/domain/models/TermsOfUse.ts b/src/info/domain/models/TermsOfUse.ts index c69bad018..96d933bfd 100644 --- a/src/info/domain/models/TermsOfUse.ts +++ b/src/info/domain/models/TermsOfUse.ts @@ -1 +1 @@ -export type TermsOfUse = string | null +export type TermsOfUse = string diff --git a/src/info/domain/repositories/DataverseInfoRepository.ts b/src/info/domain/repositories/DataverseInfoRepository.ts index 9c8cd4f3d..25f924edf 100644 --- a/src/info/domain/repositories/DataverseInfoRepository.ts +++ b/src/info/domain/repositories/DataverseInfoRepository.ts @@ -3,5 +3,5 @@ import { TermsOfUse } from '../models/TermsOfUse' export interface DataverseInfoRepository { getVersion(): Promise - getTermsOfUse: () => Promise + getApiTermsOfUse: () => Promise } diff --git a/src/info/domain/useCases/getTermsOfUse.ts b/src/info/domain/useCases/getTermsOfUse.ts index 53eaa2324..bf6f31791 100644 --- a/src/info/domain/useCases/getTermsOfUse.ts +++ b/src/info/domain/useCases/getTermsOfUse.ts @@ -1,10 +1,10 @@ import { type TermsOfUse } from '../../../info/domain/models/TermsOfUse' import { DataverseInfoRepository } from '../repositories/DataverseInfoRepository' -export function getTermsOfUse( +export function getApiTermsOfUse( dataverseInfoRepository: DataverseInfoRepository ): Promise { - return dataverseInfoRepository.getTermsOfUse().catch((error) => { + return dataverseInfoRepository.getApiTermsOfUse().catch((error) => { throw error }) } diff --git a/src/info/infrastructure/mappers/JSTermsOfUseMapper.ts b/src/info/infrastructure/mappers/JSTermsOfUseMapper.ts new file mode 100644 index 000000000..af6d859fe --- /dev/null +++ b/src/info/infrastructure/mappers/JSTermsOfUseMapper.ts @@ -0,0 +1,18 @@ +import DOMPurify from 'dompurify' +import { TermsOfUse } from '@/info/domain/models/TermsOfUse' + +export class JSTermsOfUseMapper { + static toSanitizedTermsOfUse(jsTermsOfUse: TermsOfUse): TermsOfUse { + DOMPurify.addHook('afterSanitizeAttributes', function (node) { + // set all elements owning target to target=_blank and rel=noopener for security reasons. See https://developer.chrome.com/docs/lighthouse/best-practices/external-anchors-use-rel-noopener + if ('target' in node) { + node.setAttribute('target', '_blank') + node.setAttribute('rel', 'noopener') + } + }) + // DOMPurify docs 👉 https://github.com/cure53/DOMPurify + const cleanedHTML = DOMPurify.sanitize(jsTermsOfUse, { USE_PROFILES: { html: true } }) + + return cleanedHTML + } +} diff --git a/src/info/infrastructure/repositories/DataverseInfoJSDataverseRepository.ts b/src/info/infrastructure/repositories/DataverseInfoJSDataverseRepository.ts index a7049c46a..f1627fd4e 100644 --- a/src/info/infrastructure/repositories/DataverseInfoJSDataverseRepository.ts +++ b/src/info/infrastructure/repositories/DataverseInfoJSDataverseRepository.ts @@ -3,6 +3,7 @@ import { axiosInstance } from '@/axiosInstance' import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' import { DataverseVersion } from '@/info/domain/models/DataverseVersion' import { TermsOfUse } from '@/info/domain/models/TermsOfUse' +import { JSTermsOfUseMapper } from '../mappers/JSTermsOfUseMapper' interface JSDataverseDataverseVersion { number: string @@ -29,12 +30,12 @@ export class DataverseInfoJSDataverseRepository implements DataverseInfoReposito }) } - async getTermsOfUse() { + async getApiTermsOfUse() { //TODO - implement using js-dataverse const response = await axiosInstance.get<{ data: { message: TermsOfUse } }>( '/api/v1/info/apiTermsOfUse', { excludeToken: true } ) - return response.data.data.message + return JSTermsOfUseMapper.toSanitizedTermsOfUse(response.data.data.message) } } diff --git a/src/sections/sign-up/SignUp.tsx b/src/sections/sign-up/SignUp.tsx index 16992f88f..49574ad36 100644 --- a/src/sections/sign-up/SignUp.tsx +++ b/src/sections/sign-up/SignUp.tsx @@ -8,7 +8,6 @@ import { ValidTokenNotLinkedAccountForm } from './valid-token-not-linked-account import styles from './SignUp.module.scss' // TODO:ME - All use cases will return same error message so this is blocking us for making requests to other public use cases like get root collection, should work removing access token from localstorage but we need it for future call -// TODO:ME - Ask about the format of the terms of use, html string? just text string? what is shown in the box if there is just a url string ? interface SignUpProps { userRepository: UserRepository diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.module.scss b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.module.scss index 72df568e9..54cb7a61b 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.module.scss +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.module.scss @@ -1,3 +1,5 @@ +@import 'node_modules/@iqss/dataverse-design-system/src/lib/assets/styles/design-tokens/colors.module'; + .form-container { scroll-margin-top: 62px; } @@ -15,3 +17,12 @@ text-align: left; } } + +.terms-of-use-wrapper { + max-height: 200px; + padding: 12px; + overflow-y: auto; + background-color: #f5f5f5; + border: solid 1px $dv-border-color; + border-radius: 6px; +} diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx index d19e0a00b..74e4c4795 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx @@ -271,10 +271,11 @@ export const FormFields = ({ userRepository, formDefaultValues, termsOfUse }: Fo render={({ field: { onChange, ref, value }, fieldState: { invalid, error } }) => ( - { const { tokenData } = useContext(AuthContext) - const { termsOfUse, isLoading: isLoadingTermsOfUse } = useGetTermsOfUse(dataverseInfoRepository) + const { termsOfUse, isLoading: isLoadingTermsOfUse } = + useGetApiTermsOfUse(dataverseInfoRepository) const defaultUserName = ValidTokenNotLinkedAccountFormHelper.getTokenDataValue( diff --git a/src/shared/hooks/useGetTermsOfUse.ts b/src/shared/hooks/useGetApiTermsOfUse.ts similarity index 90% rename from src/shared/hooks/useGetTermsOfUse.ts rename to src/shared/hooks/useGetApiTermsOfUse.ts index a12908ec8..5aeac761b 100644 --- a/src/shared/hooks/useGetTermsOfUse.ts +++ b/src/shared/hooks/useGetApiTermsOfUse.ts @@ -7,7 +7,7 @@ interface UseGetTermsOfUseReturnType { isLoading: boolean } -export const useGetTermsOfUse = ( +export const useGetApiTermsOfUse = ( dataverseInfoRepository: DataverseInfoRepository ): UseGetTermsOfUseReturnType => { const [termsOfUse, setTermsOfUse] = useState(null) @@ -18,7 +18,7 @@ export const useGetTermsOfUse = ( const handleGetUseOfTerms = async () => { setIsLoading(true) try { - const termsOfUse = await dataverseInfoRepository.getTermsOfUse() + const termsOfUse = await dataverseInfoRepository.getApiTermsOfUse() setTermsOfUse(termsOfUse) } catch (err) { diff --git a/src/stories/shared-mock-repositories/info/DataverseInfoMockLoadingkRepository.ts b/src/stories/shared-mock-repositories/info/DataverseInfoMockLoadingkRepository.ts index b6916f67a..f6dce5f88 100644 --- a/src/stories/shared-mock-repositories/info/DataverseInfoMockLoadingkRepository.ts +++ b/src/stories/shared-mock-repositories/info/DataverseInfoMockLoadingkRepository.ts @@ -7,7 +7,7 @@ export class DataverseInfoMockLoadingRepository implements DataverseInfoMockRepo return new Promise(() => {}) } - getTermsOfUse(): Promise { + getApiTermsOfUse(): Promise { return new Promise(() => {}) } } diff --git a/src/stories/shared-mock-repositories/info/DataverseInfoMockRepository.ts b/src/stories/shared-mock-repositories/info/DataverseInfoMockRepository.ts index 7e0671c69..cbce0e9eb 100644 --- a/src/stories/shared-mock-repositories/info/DataverseInfoMockRepository.ts +++ b/src/stories/shared-mock-repositories/info/DataverseInfoMockRepository.ts @@ -13,7 +13,7 @@ export class DataverseInfoMockRepository implements DataverseInfoRepository { }) } - getTermsOfUse(): Promise { + getApiTermsOfUse(): Promise { return new Promise((resolve) => { setTimeout(() => { resolve(TermsOfUseMother.create()) diff --git a/tests/component/info/models/TermsOfUseMother.ts b/tests/component/info/models/TermsOfUseMother.ts index 64933502d..9db4628ee 100644 --- a/tests/component/info/models/TermsOfUseMother.ts +++ b/tests/component/info/models/TermsOfUseMother.ts @@ -1,10 +1,8 @@ -import { faker } from '@faker-js/faker' -import isChromatic from 'chromatic/isChromatic' import { TermsOfUse } from '@/info/domain/models/TermsOfUse' export class TermsOfUseMother { static create(): TermsOfUse { - return isChromatic() ? 'https://some-terms-of-use-url.com' : faker.lorem.paragraphs(8) + return '

Terms of Use SPA dev

Please see our full terms of use

Thanks for reading!

' } static createEmpty(): TermsOfUse { diff --git a/tests/component/sections/sign-up/SignUp.spec.tsx b/tests/component/sections/sign-up/SignUp.spec.tsx index f359d17f8..e773d5b17 100644 --- a/tests/component/sections/sign-up/SignUp.spec.tsx +++ b/tests/component/sections/sign-up/SignUp.spec.tsx @@ -9,7 +9,7 @@ const userRepository: UserRepository = {} as UserRepository describe('SignUp', () => { beforeEach(() => { - dataverseInfoRepository.getTermsOfUse = cy.stub().resolves('Terms of use') + dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves('Terms of use') }) it('renders the valid token not linked account form and correct alerts when hasValidTokenButNotLinkedAccount prop is true', () => { diff --git a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx index 369027d91..54a8c5501 100644 --- a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx +++ b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx @@ -4,11 +4,15 @@ import { UserRepository } from '@/users/domain/repositories/UserRepository' import { ValidTokenNotLinkedAccountForm } from '@/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm' import { AuthContextMother } from '@tests/component/auth/AuthContextMother' import { UserDTO } from '@/users/domain/useCases/DTOs/UserDTO' +import { TermsOfUseMother } from '@tests/component/info/models/TermsOfUseMother' +import { JSTermsOfUseMapper } from '@/info/infrastructure/mappers/JSTermsOfUseMapper' const dataverseInfoRepository: DataverseInfoRepository = {} as DataverseInfoRepository const userRepository: UserRepository = {} as UserRepository -const termsOfUseMock = 'Terms of use' +const termsOfUseMock = TermsOfUseMother.create() +const sanitizedTermsOfUseMock = JSTermsOfUseMapper.toSanitizedTermsOfUse(termsOfUseMock) + const mockUserName = 'mockUserName' const mockFirstName = 'mockFirstName' const mockLastName = 'mockLastName' @@ -16,7 +20,7 @@ const mockEmail = 'mockEmail@email.com' describe('ValidTokenNotLinkedAccountForm', () => { beforeEach(() => { - dataverseInfoRepository.getTermsOfUse = cy.stub().resolves(termsOfUseMock) + dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves(sanitizedTermsOfUseMock) userRepository.register = cy.stub().as('registerUser').resolves() }) @@ -51,7 +55,7 @@ describe('ValidTokenNotLinkedAccountForm', () => { cy.findByLabelText('Given Name').should('have.value', mockFirstName) cy.findByLabelText('Family Name').should('have.value', mockLastName) cy.findByLabelText('Email').should('have.value', mockEmail) - cy.findByText(termsOfUseMock).should('exist') + cy.findByText('Terms of Use SPA dev').should('exist') }) it('renders the form fields with the correct default values when tokenData does not have preferred username, given name, family name and email', () => { @@ -79,7 +83,7 @@ describe('ValidTokenNotLinkedAccountForm', () => { cy.findByLabelText('Given Name').should('have.value', '') cy.findByLabelText('Family Name').should('have.value', '') cy.findByLabelText('Email').should('have.value', '') - cy.findByText(termsOfUseMock).should('exist') + cy.findByText('Terms of Use SPA dev').should('exist') }) }) @@ -216,7 +220,7 @@ describe('ValidTokenNotLinkedAccountForm', () => { }) it('shows no terms message when there are no terms of use', () => { - dataverseInfoRepository.getTermsOfUse = cy.stub().resolves(null) + dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves(null) cy.customMount( { +describe('useGetApiTermsOfUse', () => { it('should return terms of use correctly', async () => { - dataverseInfoRepository.getTermsOfUse = cy.stub().resolves(termsOfUseMock) + dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves(termsOfUseMock) - const { result } = renderHook(() => useGetTermsOfUse(dataverseInfoRepository)) + const { result } = renderHook(() => useGetApiTermsOfUse(dataverseInfoRepository)) await act(() => { expect(result.current.isLoading).to.deep.equal(true) @@ -26,9 +26,9 @@ describe('useGetTermsOfUse', () => { describe('Error handling', () => { it('should return correct error message when there is an error type catched', async () => { - dataverseInfoRepository.getTermsOfUse = cy.stub().rejects(new Error('Error message')) + dataverseInfoRepository.getApiTermsOfUse = cy.stub().rejects(new Error('Error message')) - const { result } = renderHook(() => useGetTermsOfUse(dataverseInfoRepository)) + const { result } = renderHook(() => useGetApiTermsOfUse(dataverseInfoRepository)) await act(() => { expect(result.current.isLoading).to.deep.equal(true) @@ -42,9 +42,9 @@ describe('useGetTermsOfUse', () => { }) it('should return correct error message when there is not an error type catched', async () => { - dataverseInfoRepository.getTermsOfUse = cy.stub().rejects('Error message') + dataverseInfoRepository.getApiTermsOfUse = cy.stub().rejects('Error message') - const { result } = renderHook(() => useGetTermsOfUse(dataverseInfoRepository)) + const { result } = renderHook(() => useGetApiTermsOfUse(dataverseInfoRepository)) await act(() => { expect(result.current.isLoading).to.deep.equal(true) From ae17898adf0aa85b975acbe9c3213a947991b132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 3 Dec 2024 15:21:24 -0300 Subject: [PATCH 77/97] lint: fix type --- src/shared/hooks/useGetApiTermsOfUse.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/hooks/useGetApiTermsOfUse.ts b/src/shared/hooks/useGetApiTermsOfUse.ts index 5aeac761b..1ec833167 100644 --- a/src/shared/hooks/useGetApiTermsOfUse.ts +++ b/src/shared/hooks/useGetApiTermsOfUse.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' interface UseGetTermsOfUseReturnType { - termsOfUse: string | null + termsOfUse: string error: string | null isLoading: boolean } @@ -10,7 +10,7 @@ interface UseGetTermsOfUseReturnType { export const useGetApiTermsOfUse = ( dataverseInfoRepository: DataverseInfoRepository ): UseGetTermsOfUseReturnType => { - const [termsOfUse, setTermsOfUse] = useState(null) + const [termsOfUse, setTermsOfUse] = useState('') const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) From 1f84839ebf8e9503f8ab263d700879ae9606cbd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 3 Dec 2024 15:49:46 -0300 Subject: [PATCH 78/97] test: fix unit test hook --- .../{useGetTermsOfUse.spec.ts => useGetApiTermsOfUse.spec.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/component/shared/hooks/{useGetTermsOfUse.spec.ts => useGetApiTermsOfUse.spec.ts} (97%) diff --git a/tests/component/shared/hooks/useGetTermsOfUse.spec.ts b/tests/component/shared/hooks/useGetApiTermsOfUse.spec.ts similarity index 97% rename from tests/component/shared/hooks/useGetTermsOfUse.spec.ts rename to tests/component/shared/hooks/useGetApiTermsOfUse.spec.ts index 4a626c9aa..161473cf2 100644 --- a/tests/component/shared/hooks/useGetTermsOfUse.spec.ts +++ b/tests/component/shared/hooks/useGetApiTermsOfUse.spec.ts @@ -14,7 +14,7 @@ describe('useGetApiTermsOfUse', () => { await act(() => { expect(result.current.isLoading).to.deep.equal(true) - return expect(result.current.termsOfUse).to.deep.equal(null) + return expect(result.current.termsOfUse).to.deep.equal('') }) await act(() => { From 655020606ac5a72bc761319057f324cc01a1cf4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 3 Dec 2024 16:44:30 -0300 Subject: [PATCH 79/97] chore: update to latest js-dataverse pr version --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index f772a9ed9..4fc3e7a1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-pr224.a5634ad", + "@iqss/dataverse-client-javascript": "2.0.0-pr224.5f50318", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -3677,9 +3677,9 @@ }, "node_modules/@iqss/dataverse-client-javascript": { "name": "@IQSS/dataverse-client-javascript", - "version": "2.0.0-pr224.a5634ad", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr224.a5634ad/efb3fffecf1baf5f686a2183d9703bde584e3f18", - "integrity": "sha512-HupVa//v5Q1Fo3Ln5Ia1pqiDhjR6Zl9MmydOgvxgZROdGNeuFoI6unWd/GghEbaPMSM5QHvtE5oHhQQjBlFnOg==", + "version": "2.0.0-pr224.5f50318", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr224.5f50318/edd4e7df6bd2d11257b3687a1a4958effc99cfc2", + "integrity": "sha512-3BNP1U1/mKGomeCmWdf3Onoh9tEKd7+n9iDrI2luXxMFoXMF8poWd5O5a1OOm6eeXGg0dQRkQL2WIt0rW6WskA==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", diff --git a/package.json b/package.json index 35be31b5f..270d654fe 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-pr224.a5634ad", + "@iqss/dataverse-client-javascript": "2.0.0-pr224.5f50318", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", From 67de855f93d19584ff56f70d32452b85d5f81350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 4 Dec 2024 09:55:29 -0300 Subject: [PATCH 80/97] feat: comment out terms of use call --- .../ValidTokenNotLinkedAccountForm.tsx | 18 ++++++++++-------- .../ValidTokenNotLinkedAccountForm.spec.tsx | 16 +++++++++------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx index 7e10fcaf6..e55a61d6d 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx @@ -2,11 +2,11 @@ import { useContext } from 'react' import { AuthContext } from 'react-oauth2-code-pkce' import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' import { UserRepository } from '@/users/domain/repositories/UserRepository' -import { useGetApiTermsOfUse } from '@/shared/hooks/useGetApiTermsOfUse' +// import { useGetApiTermsOfUse } from '@/shared/hooks/useGetApiTermsOfUse' import { OIDC_STANDARD_CLAIMS, type ValidTokenNotLinkedAccountFormData } from './types' import { ValidTokenNotLinkedAccountFormHelper } from './ValidTokenNotLinkedAccountFormHelper' import { FormFields } from './FormFields' -import { FormFieldsSkeleton } from './FormFieldsSkeleton' +// import { FormFieldsSkeleton } from './FormFieldsSkeleton' interface ValidTokenNotLinkedAccountFormProps { dataverseInfoRepository: DataverseInfoRepository @@ -18,8 +18,10 @@ export const ValidTokenNotLinkedAccountForm = ({ dataverseInfoRepository }: ValidTokenNotLinkedAccountFormProps) => { const { tokenData } = useContext(AuthContext) - const { termsOfUse, isLoading: isLoadingTermsOfUse } = - useGetApiTermsOfUse(dataverseInfoRepository) + + // TODO - Change for application terms of use when available in API 👇 + // const { termsOfUse, isLoading: isLoadingTermsOfUse } = + // useGetApiTermsOfUse(dataverseInfoRepository) const defaultUserName = ValidTokenNotLinkedAccountFormHelper.getTokenDataValue( @@ -59,15 +61,15 @@ export const ValidTokenNotLinkedAccountForm = ({ termsAccepted: false } - if (isLoadingTermsOfUse) { - return - } + // if (isLoadingTermsOfUse) { + // return + // } return ( ) } diff --git a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx index 54a8c5501..c6f166e78 100644 --- a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx +++ b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx @@ -4,14 +4,15 @@ import { UserRepository } from '@/users/domain/repositories/UserRepository' import { ValidTokenNotLinkedAccountForm } from '@/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm' import { AuthContextMother } from '@tests/component/auth/AuthContextMother' import { UserDTO } from '@/users/domain/useCases/DTOs/UserDTO' -import { TermsOfUseMother } from '@tests/component/info/models/TermsOfUseMother' -import { JSTermsOfUseMapper } from '@/info/infrastructure/mappers/JSTermsOfUseMapper' +// import { TermsOfUseMother } from '@tests/component/info/models/TermsOfUseMother' +// import { JSTermsOfUseMapper } from '@/info/infrastructure/mappers/JSTermsOfUseMapper' const dataverseInfoRepository: DataverseInfoRepository = {} as DataverseInfoRepository const userRepository: UserRepository = {} as UserRepository -const termsOfUseMock = TermsOfUseMother.create() -const sanitizedTermsOfUseMock = JSTermsOfUseMapper.toSanitizedTermsOfUse(termsOfUseMock) +// TODO - Uncomment when application terms of use are available in API +// const termsOfUseMock = TermsOfUseMother.create() +// const sanitizedTermsOfUseMock = JSTermsOfUseMapper.toSanitizedTermsOfUse(termsOfUseMock) const mockUserName = 'mockUserName' const mockFirstName = 'mockFirstName' @@ -20,7 +21,8 @@ const mockEmail = 'mockEmail@email.com' describe('ValidTokenNotLinkedAccountForm', () => { beforeEach(() => { - dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves(sanitizedTermsOfUseMock) + // dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves(sanitizedTermsOfUseMock) + dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves('') userRepository.register = cy.stub().as('registerUser').resolves() }) @@ -55,7 +57,7 @@ describe('ValidTokenNotLinkedAccountForm', () => { cy.findByLabelText('Given Name').should('have.value', mockFirstName) cy.findByLabelText('Family Name').should('have.value', mockLastName) cy.findByLabelText('Email').should('have.value', mockEmail) - cy.findByText('Terms of Use SPA dev').should('exist') + // cy.findByText('Terms of Use SPA dev').should('exist') }) it('renders the form fields with the correct default values when tokenData does not have preferred username, given name, family name and email', () => { @@ -83,7 +85,7 @@ describe('ValidTokenNotLinkedAccountForm', () => { cy.findByLabelText('Given Name').should('have.value', '') cy.findByLabelText('Family Name').should('have.value', '') cy.findByLabelText('Email').should('have.value', '') - cy.findByText('Terms of Use SPA dev').should('exist') + // cy.findByText('Terms of Use SPA dev').should('exist') }) }) From 93c4d4b0f0249e7b64d45554222ec71fc9f2281d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 4 Dec 2024 09:56:11 -0300 Subject: [PATCH 81/97] test: update accoun info tests --- .../account/AccountInfoSection.spec.tsx | 21 +++++++++++++++++++ .../users/domain/models/UserMother.ts | 5 +++-- tests/support/commands.tsx | 5 +++-- tests/support/component.ts | 4 +++- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/tests/component/sections/account/AccountInfoSection.spec.tsx b/tests/component/sections/account/AccountInfoSection.spec.tsx index 1fc9d6a9e..614dbcd39 100644 --- a/tests/component/sections/account/AccountInfoSection.spec.tsx +++ b/tests/component/sections/account/AccountInfoSection.spec.tsx @@ -29,4 +29,25 @@ describe('AccountInfoSection', () => { }) }) }) + + it('should display the user affiliation and position if present', () => { + cy.mountAuthenticated(, undefined, { + affiliation: 'Harvard University', + position: 'Researcher' + }) + + cy.findAllByRole('row').spread( + (_usernameRow, _givenNameRow, _familyNameRow, _emailRow, affiliationRow, positionRow) => { + cy.wrap(affiliationRow).within(() => { + cy.findByText('Affiliation').should('exist') + cy.findByText('Harvard University').should('exist') + }) + + cy.wrap(positionRow).within(() => { + cy.findByText('Position').should('exist') + cy.findByText('Researcher').should('exist') + }) + } + ) + }) }) diff --git a/tests/component/users/domain/models/UserMother.ts b/tests/component/users/domain/models/UserMother.ts index 92a06fb95..2ef8f266c 100644 --- a/tests/component/users/domain/models/UserMother.ts +++ b/tests/component/users/domain/models/UserMother.ts @@ -1,7 +1,7 @@ import { User } from '../../../../../src/users/domain/models/User' export class UserMother { - static create(): User { + static create(props?: Partial): User { return { displayName: 'James D. Potts', persistentId: 'jamesPotts', @@ -10,7 +10,8 @@ export class UserMother { email: 'jamesPotts@g.harvard.edu', affiliation: 'Harvard University', superuser: false, - identifier: 'jamespotts' + identifier: 'jamespotts', + ...props } } static createSuperUser(): User { diff --git a/tests/support/commands.tsx b/tests/support/commands.tsx index 2810cf4f4..95f7a309f 100644 --- a/tests/support/commands.tsx +++ b/tests/support/commands.tsx @@ -48,6 +48,7 @@ import { TestsUtils } from '@tests/e2e-integration/shared/TestsUtils' import { Utils } from '@/shared/helpers/Utils' import { OIDC_AUTH_CONFIG } from '@/config' import { SessionContext } from '@/sections/session/SessionContext' +import { User } from '@/users/domain/models/User' // Define your custom mount function @@ -68,11 +69,11 @@ Cypress.Commands.add( Cypress.Commands.add( 'mountAuthenticated', - (component: ReactNode, initialEntries?: RouterInitialEntry[]) => { + (component: ReactNode, initialEntries?: RouterInitialEntry[], userOverrides?: Partial) => { return cy.customMount( Promise.resolve(), setUser: () => {}, isLoadingUser: false, diff --git a/tests/support/component.ts b/tests/support/component.ts index 44f019eb7..48aedb354 100644 --- a/tests/support/component.ts +++ b/tests/support/component.ts @@ -24,6 +24,7 @@ import 'react-loading-skeleton/dist/skeleton.css' import { mount, MountReturn } from 'cypress/react18' import { RouterInitialEntry } from './commands' import { ReactNode } from 'react' +import { User } from '@/users/domain/models/User' // Augment the Cypress namespace to include type definitions for // your custom command. @@ -42,7 +43,8 @@ declare global { ) => Cypress.Chainable mountAuthenticated: ( component: ReactNode, - initialEntries?: RouterInitialEntry[] + initialEntries?: RouterInitialEntry[], + userOverrides?: Partial ) => Cypress.Chainable mountSuperuser: ( component: ReactNode, From d04cd5a3640654f861dae38c930fcd6be9f48751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 4 Dec 2024 11:51:18 -0300 Subject: [PATCH 82/97] test: force click --- .../ValidTokenNotLinkedAccountForm.spec.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx index c6f166e78..fc06e1394 100644 --- a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx +++ b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx @@ -125,7 +125,7 @@ describe('ValidTokenNotLinkedAccountForm', () => { 'I have read and accept the Dataverse General Terms of Use as outlined above.' ).check({ force: true }) - cy.findByRole('button', { name: 'Create Account' }).click() + cy.findByRole('button', { name: 'Create Account' }).click({ force: true }) cy.get('@registerUser').should((spy) => { const registerUserSpy = spy as unknown as Cypress.Agent @@ -185,7 +185,7 @@ describe('ValidTokenNotLinkedAccountForm', () => { cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') - cy.findByRole('button', { name: 'Create Account' }).click() + cy.findByRole('button', { name: 'Create Account' }).click({ force: true }) // Assert that the form has errors in Username and Email fields cy.findByText('Username is required.').should('exist') @@ -204,7 +204,7 @@ describe('ValidTokenNotLinkedAccountForm', () => { cy.findByLabelText('Family Name').type(newMockLastName) cy.findByLabelText('Email').type(newMockEmail) - cy.findByRole('button', { name: 'Create Account' }).click() + cy.findByRole('button', { name: 'Create Account' }).click({ force: true }) cy.get('@registerUser').should((spy) => { const registerUserSpy = spy as unknown as Cypress.Agent From 0ba8559ea9192423cfda4e23c9c881301b5e360d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 4 Dec 2024 13:32:13 -0300 Subject: [PATCH 83/97] test: wait to see if button disabled error goes away --- .../ValidTokenNotLinkedAccountForm.spec.tsx | 8 ++++++++ tests/e2e-integration/shared/DataverseApiHelper.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx index fc06e1394..6b79d7c8f 100644 --- a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx +++ b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx @@ -125,6 +125,8 @@ describe('ValidTokenNotLinkedAccountForm', () => { 'I have read and accept the Dataverse General Terms of Use as outlined above.' ).check({ force: true }) + cy.wait(500) + cy.findByRole('button', { name: 'Create Account' }).click({ force: true }) cy.get('@registerUser').should((spy) => { @@ -183,6 +185,8 @@ describe('ValidTokenNotLinkedAccountForm', () => { 'I have read and accept the Dataverse General Terms of Use as outlined above.' ).check({ force: true }) + cy.wait(500) + cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') cy.findByRole('button', { name: 'Create Account' }).click({ force: true }) @@ -204,6 +208,10 @@ describe('ValidTokenNotLinkedAccountForm', () => { cy.findByLabelText('Family Name').type(newMockLastName) cy.findByLabelText('Email').type(newMockEmail) + cy.wait(500) + + cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') + cy.findByRole('button', { name: 'Create Account' }).click({ force: true }) cy.get('@registerUser').should((spy) => { diff --git a/tests/e2e-integration/shared/DataverseApiHelper.ts b/tests/e2e-integration/shared/DataverseApiHelper.ts index dce9343e2..0e6d99242 100644 --- a/tests/e2e-integration/shared/DataverseApiHelper.ts +++ b/tests/e2e-integration/shared/DataverseApiHelper.ts @@ -110,7 +110,7 @@ export class DataverseApiHelper { const API_KEY_USER_ENDPOINT = '/builtin-users/dataverseAdmin/api-token' const API_KEY_USER_PASSWORD = 'admin1' - // Get API key from superuser dataverseAdmin + // Allow token lookup via API await axios.put(`${this.API_URL}${API_ALLOW_TOKEN_LOOKUP_ENDPOINT}`, 'true') // Get API key from superuser dataverseAdmin From 7863b958a03c7cd1cb9f66684284b4b2fd20f0bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 4 Dec 2024 14:38:56 -0300 Subject: [PATCH 84/97] test: rollback changes and add missing register --- .../sections/account/ApiTokenSection.spec.tsx | 3 ++- .../ValidTokenNotLinkedAccountForm.spec.tsx | 16 ++++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/tests/component/sections/account/ApiTokenSection.spec.tsx b/tests/component/sections/account/ApiTokenSection.spec.tsx index 83547deee..8fc4d8d8e 100644 --- a/tests/component/sections/account/ApiTokenSection.spec.tsx +++ b/tests/component/sections/account/ApiTokenSection.spec.tsx @@ -20,7 +20,8 @@ describe('ApiTokenSection', () => { recreateApiToken: cy.stub().resolves(mockApiTokenInfo), deleteApiToken: cy.stub().resolves(), getAuthenticated: cy.stub().resolves(), - removeAuthenticated: cy.stub().resolves() + removeAuthenticated: cy.stub().resolves(), + register: cy.stub().resolves() } cy.mountAuthenticated() diff --git a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx index 6b79d7c8f..fdadd8fb4 100644 --- a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx +++ b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx @@ -19,7 +19,7 @@ const mockFirstName = 'mockFirstName' const mockLastName = 'mockLastName' const mockEmail = 'mockEmail@email.com' -describe('ValidTokenNotLinkedAccountForm', () => { +describe.only('ValidTokenNotLinkedAccountForm', () => { beforeEach(() => { // dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves(sanitizedTermsOfUseMock) dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves('') @@ -125,9 +125,7 @@ describe('ValidTokenNotLinkedAccountForm', () => { 'I have read and accept the Dataverse General Terms of Use as outlined above.' ).check({ force: true }) - cy.wait(500) - - cy.findByRole('button', { name: 'Create Account' }).click({ force: true }) + cy.findByRole('button', { name: 'Create Account' }).click() cy.get('@registerUser').should((spy) => { const registerUserSpy = spy as unknown as Cypress.Agent @@ -185,11 +183,9 @@ describe('ValidTokenNotLinkedAccountForm', () => { 'I have read and accept the Dataverse General Terms of Use as outlined above.' ).check({ force: true }) - cy.wait(500) - cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') - cy.findByRole('button', { name: 'Create Account' }).click({ force: true }) + cy.findByRole('button', { name: 'Create Account' }).click() // Assert that the form has errors in Username and Email fields cy.findByText('Username is required.').should('exist') @@ -208,11 +204,7 @@ describe('ValidTokenNotLinkedAccountForm', () => { cy.findByLabelText('Family Name').type(newMockLastName) cy.findByLabelText('Email').type(newMockEmail) - cy.wait(500) - - cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') - - cy.findByRole('button', { name: 'Create Account' }).click({ force: true }) + cy.findByRole('button', { name: 'Create Account' }).click() cy.get('@registerUser').should((spy) => { const registerUserSpy = spy as unknown as Cypress.Agent From f55d1902eee307418e56a097e8688199d9f6b105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 4 Dec 2024 15:55:18 -0300 Subject: [PATCH 85/97] chore: remove unusude dataverse info repo --- src/sections/sign-up/SignUp.tsx | 13 ++----------- src/sections/sign-up/SignUpFactory.tsx | 4 +--- .../ValidTokenNotLinkedAccountForm.tsx | 5 +---- src/stories/sign-up/SignUp.stories.tsx | 18 +----------------- .../component/sections/sign-up/SignUp.spec.tsx | 12 ++---------- 5 files changed, 7 insertions(+), 45 deletions(-) diff --git a/src/sections/sign-up/SignUp.tsx b/src/sections/sign-up/SignUp.tsx index 49574ad36..fcf3c99d6 100644 --- a/src/sections/sign-up/SignUp.tsx +++ b/src/sections/sign-up/SignUp.tsx @@ -1,7 +1,6 @@ import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Alert, Tabs } from '@iqss/dataverse-design-system' -import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' import { UserRepository } from '@/users/domain/repositories/UserRepository' import { useLoading } from '../loading/LoadingContext' import { ValidTokenNotLinkedAccountForm } from './valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm' @@ -11,15 +10,10 @@ import styles from './SignUp.module.scss' interface SignUpProps { userRepository: UserRepository - dataverseInfoRepository: DataverseInfoRepository hasValidTokenButNotLinkedAccount: boolean } -export const SignUp = ({ - userRepository, - dataverseInfoRepository, - hasValidTokenButNotLinkedAccount -}: SignUpProps) => { +export const SignUp = ({ userRepository, hasValidTokenButNotLinkedAccount }: SignUpProps) => { const { t } = useTranslation('signUp') const { setIsLoading } = useLoading() @@ -62,10 +56,7 @@ export const SignUp = ({
{hasValidTokenButNotLinkedAccount && ( - + )}
diff --git a/src/sections/sign-up/SignUpFactory.tsx b/src/sections/sign-up/SignUpFactory.tsx index 23810054e..50f6a4e2c 100644 --- a/src/sections/sign-up/SignUpFactory.tsx +++ b/src/sections/sign-up/SignUpFactory.tsx @@ -2,10 +2,9 @@ import { ReactElement } from 'react' import { useSearchParams } from 'react-router-dom' import { SignUp } from './SignUp' import { QueryParamKey } from '../Route.enum' -import { DataverseInfoJSDataverseRepository } from '@/info/infrastructure/repositories/DataverseInfoJSDataverseRepository' + import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository' -const dataverseInfoRepository = new DataverseInfoJSDataverseRepository() const userRepository = new UserJSDataverseRepository() export class SignUpFactory { @@ -22,7 +21,6 @@ function SignUpWithSearchParams() { return ( diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx index e55a61d6d..e569d433b 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.tsx @@ -1,6 +1,5 @@ import { useContext } from 'react' import { AuthContext } from 'react-oauth2-code-pkce' -import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' import { UserRepository } from '@/users/domain/repositories/UserRepository' // import { useGetApiTermsOfUse } from '@/shared/hooks/useGetApiTermsOfUse' import { OIDC_STANDARD_CLAIMS, type ValidTokenNotLinkedAccountFormData } from './types' @@ -9,13 +8,11 @@ import { FormFields } from './FormFields' // import { FormFieldsSkeleton } from './FormFieldsSkeleton' interface ValidTokenNotLinkedAccountFormProps { - dataverseInfoRepository: DataverseInfoRepository userRepository: UserRepository } export const ValidTokenNotLinkedAccountForm = ({ - userRepository, - dataverseInfoRepository + userRepository }: ValidTokenNotLinkedAccountFormProps) => { const { tokenData } = useContext(AuthContext) diff --git a/src/stories/sign-up/SignUp.stories.tsx b/src/stories/sign-up/SignUp.stories.tsx index cea400d08..fce140863 100644 --- a/src/stories/sign-up/SignUp.stories.tsx +++ b/src/stories/sign-up/SignUp.stories.tsx @@ -2,8 +2,6 @@ import type { StoryObj, Meta } from '@storybook/react' import { WithLayout } from '../WithLayout' import { WithI18next } from '../WithI18next' import { SignUp } from '@/sections/sign-up/SignUp' -import { DataverseInfoMockRepository } from '../shared-mock-repositories/info/DataverseInfoMockRepository' -import { DataverseInfoMockLoadingRepository } from '../shared-mock-repositories/info/DataverseInfoMockLoadingkRepository' import { WithOIDCAuthContext } from '../WithOIDCAuthContext' import { UserMockRepository } from '../shared-mock-repositories/user/UserMockRepository' @@ -21,20 +19,6 @@ type Story = StoryObj export const ValidTokenWithNotLinkedAccount: Story = { render: () => ( - - ) -} - -export const Loading: Story = { - render: () => ( - + ) } diff --git a/tests/component/sections/sign-up/SignUp.spec.tsx b/tests/component/sections/sign-up/SignUp.spec.tsx index e773d5b17..0ccf1ee71 100644 --- a/tests/component/sections/sign-up/SignUp.spec.tsx +++ b/tests/component/sections/sign-up/SignUp.spec.tsx @@ -26,11 +26,7 @@ describe('SignUp', () => { error: null, login: () => {} // 👈 deprecated }}> - +
) @@ -56,11 +52,7 @@ describe('SignUp', () => { error: null, login: () => {} // 👈 deprecated }}> - + ) From 9eaf379d91c383fe82017751f7a036e0d7c5ce00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 4 Dec 2024 15:55:41 -0300 Subject: [PATCH 86/97] test: change cy viewport --- .../ValidTokenNotLinkedAccountForm.spec.tsx | 34 +++++-------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx index fdadd8fb4..cdef1072d 100644 --- a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx +++ b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx @@ -19,8 +19,10 @@ const mockFirstName = 'mockFirstName' const mockLastName = 'mockLastName' const mockEmail = 'mockEmail@email.com' -describe.only('ValidTokenNotLinkedAccountForm', () => { +describe('ValidTokenNotLinkedAccountForm', () => { beforeEach(() => { + cy.viewport(1280, 720) + // dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves(sanitizedTermsOfUseMock) dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves('') userRepository.register = cy.stub().as('registerUser').resolves() @@ -46,10 +48,7 @@ describe.only('ValidTokenNotLinkedAccountForm', () => { error: null, login: () => {} // 👈 deprecated }}> - + ) @@ -74,10 +73,7 @@ describe.only('ValidTokenNotLinkedAccountForm', () => { error: null, login: () => {} // 👈 deprecated }}> - + ) @@ -114,10 +110,7 @@ describe.only('ValidTokenNotLinkedAccountForm', () => { error: null, login: () => {} // 👈 deprecated }}> - + ) @@ -151,10 +144,7 @@ describe.only('ValidTokenNotLinkedAccountForm', () => { error: null, login: () => {} // 👈 deprecated }}> - + ) @@ -237,10 +227,7 @@ describe.only('ValidTokenNotLinkedAccountForm', () => { error: null, login: () => {} // 👈 deprecated }}> - + ) @@ -263,10 +250,7 @@ describe.only('ValidTokenNotLinkedAccountForm', () => { error: null, login: () => {} // 👈 deprecated }}> - + ) From c5e9031e7a7c1dbeba3ca58081b6c19b7d40c2d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 4 Dec 2024 16:46:00 -0300 Subject: [PATCH 87/97] test: set up cypress cloud recording --- .github/workflows/test.yml | 4 ++++ cypress.config.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ca11b5d15..34ae7b0f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -122,6 +122,10 @@ jobs: uses: cypress-io/github-action@v5 with: component: true + record: true + env: + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Cypress run Design System uses: cypress-io/github-action@v5 diff --git a/cypress.config.ts b/cypress.config.ts index 630cc27ff..3d5be5f45 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -3,6 +3,7 @@ import vitePreprocessor from 'cypress-vite' import path from 'path' export default defineConfig({ + projectId: '6sn9cr', video: false, e2e: { baseUrl: 'http://localhost:8000', From 4c1de693a439745206129710c3ae3a1d2431eb3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 4 Dec 2024 17:13:08 -0300 Subject: [PATCH 88/97] test: check with find by test id --- .../FormFields.tsx | 1 + .../ValidTokenNotLinkedAccountForm.spec.tsx | 17 ++++------------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx index 74e4c4795..0759b6f3a 100644 --- a/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx +++ b/src/sections/sign-up/valid-token-not-linked-account-form/FormFields.tsx @@ -280,6 +280,7 @@ export const FormFields = ({ userRepository, formDefaultValues, termsOfUse }: Fo { ) - - cy.findByLabelText( - 'I have read and accept the Dataverse General Terms of Use as outlined above.' - ).check({ force: true }) + cy.findByTestId('termsAcceptedCheckbox').check({ force: true }) cy.findByRole('button', { name: 'Create Account' }).click() @@ -156,22 +153,16 @@ describe('ValidTokenNotLinkedAccountForm', () => { // Assert that submit button is disabled if terms are not accepted cy.findByRole('button', { name: 'Create Account' }).should('be.disabled') - cy.findByLabelText( - 'I have read and accept the Dataverse General Terms of Use as outlined above.' - ).check({ force: true }) + cy.findByTestId('termsAcceptedCheckbox').check({ force: true }) // Uncheck and then check again to test validation error from terms not accepted - cy.findByLabelText( - 'I have read and accept the Dataverse General Terms of Use as outlined above.' - ).uncheck({ force: true }) + cy.findByTestId('termsAcceptedCheckbox').uncheck({ force: true }) cy.findByText( 'Please check the box to indicate your acceptance of the General Terms of Use.' ).should('exist') - cy.findByLabelText( - 'I have read and accept the Dataverse General Terms of Use as outlined above.' - ).check({ force: true }) + cy.findByTestId('termsAcceptedCheckbox').check({ force: true }) cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') From 92ff47206a8b483be42fe320b1efbbdddd9193b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 4 Dec 2024 17:30:34 -0300 Subject: [PATCH 89/97] test: check not disabled first --- .../ValidTokenNotLinkedAccountForm.spec.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx index e30cd7db1..0670893c8 100644 --- a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx +++ b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx @@ -86,7 +86,7 @@ describe('ValidTokenNotLinkedAccountForm', () => { }) describe('submit form with correct data', () => { - it('submits the form with the correct data when tokenData has preferred username, given name, family name and email ', () => { + it.only('submits the form with the correct data when tokenData has preferred username, given name, family name and email ', () => { cy.customMount( { ) cy.findByTestId('termsAcceptedCheckbox').check({ force: true }) + cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') + cy.findByRole('button', { name: 'Create Account' }).click() cy.get('@registerUser').should((spy) => { @@ -155,6 +157,8 @@ describe('ValidTokenNotLinkedAccountForm', () => { cy.findByTestId('termsAcceptedCheckbox').check({ force: true }) + cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') + // Uncheck and then check again to test validation error from terms not accepted cy.findByTestId('termsAcceptedCheckbox').uncheck({ force: true }) From 27c1d280a31d2ff06c3d8228e8a97b6284d3f580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 4 Dec 2024 17:46:15 -0300 Subject: [PATCH 90/97] test: check not disabled and checked states before clicking --- .../ValidTokenNotLinkedAccountForm.spec.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx index 0670893c8..7cd07715a 100644 --- a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx +++ b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx @@ -86,7 +86,7 @@ describe('ValidTokenNotLinkedAccountForm', () => { }) describe('submit form with correct data', () => { - it.only('submits the form with the correct data when tokenData has preferred username, given name, family name and email ', () => { + it('submits the form with the correct data when tokenData has preferred username, given name, family name and email ', () => { cy.customMount( { ) cy.findByTestId('termsAcceptedCheckbox').check({ force: true }) + cy.findByTestId('termsAcceptedCheckbox').should('be.checked') + cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') cy.findByRole('button', { name: 'Create Account' }).click() @@ -157,17 +159,23 @@ describe('ValidTokenNotLinkedAccountForm', () => { cy.findByTestId('termsAcceptedCheckbox').check({ force: true }) + cy.findByTestId('termsAcceptedCheckbox').should('be.checked') + cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') // Uncheck and then check again to test validation error from terms not accepted cy.findByTestId('termsAcceptedCheckbox').uncheck({ force: true }) + cy.findByTestId('termsAcceptedCheckbox').should('not.be.checked') + cy.findByText( 'Please check the box to indicate your acceptance of the General Terms of Use.' ).should('exist') cy.findByTestId('termsAcceptedCheckbox').check({ force: true }) + cy.findByTestId('termsAcceptedCheckbox').should('be.checked') + cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') cy.findByRole('button', { name: 'Create Account' }).click() From 31d25dea7559e4a526ffe300948fb64bde675355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 4 Dec 2024 21:16:57 -0300 Subject: [PATCH 91/97] test: remove should be checked --- .../ValidTokenNotLinkedAccountForm.spec.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx index 7cd07715a..5b3d98b2c 100644 --- a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx +++ b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx @@ -85,6 +85,7 @@ describe('ValidTokenNotLinkedAccountForm', () => { }) }) + // TODO:ME - Remove setting up record of component test and remove secret from github describe('submit form with correct data', () => { it('submits the form with the correct data when tokenData has preferred username, given name, family name and email ', () => { cy.customMount( @@ -115,8 +116,6 @@ describe('ValidTokenNotLinkedAccountForm', () => { ) cy.findByTestId('termsAcceptedCheckbox').check({ force: true }) - cy.findByTestId('termsAcceptedCheckbox').should('be.checked') - cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') cy.findByRole('button', { name: 'Create Account' }).click() @@ -159,23 +158,17 @@ describe('ValidTokenNotLinkedAccountForm', () => { cy.findByTestId('termsAcceptedCheckbox').check({ force: true }) - cy.findByTestId('termsAcceptedCheckbox').should('be.checked') - cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') // Uncheck and then check again to test validation error from terms not accepted cy.findByTestId('termsAcceptedCheckbox').uncheck({ force: true }) - cy.findByTestId('termsAcceptedCheckbox').should('not.be.checked') - cy.findByText( 'Please check the box to indicate your acceptance of the General Terms of Use.' ).should('exist') cy.findByTestId('termsAcceptedCheckbox').check({ force: true }) - cy.findByTestId('termsAcceptedCheckbox').should('be.checked') - cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') cy.findByRole('button', { name: 'Create Account' }).click() From 31f506a753a181bc361f71db7b32c6c84a7668bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 4 Dec 2024 21:50:50 -0300 Subject: [PATCH 92/97] test: add wait 300 --- .../ValidTokenNotLinkedAccountForm.spec.tsx | 38 ++++++------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx index 5b3d98b2c..94a2ac35d 100644 --- a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx +++ b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx @@ -1,13 +1,13 @@ import { AuthContext } from 'react-oauth2-code-pkce' -import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' import { UserRepository } from '@/users/domain/repositories/UserRepository' import { ValidTokenNotLinkedAccountForm } from '@/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm' import { AuthContextMother } from '@tests/component/auth/AuthContextMother' import { UserDTO } from '@/users/domain/useCases/DTOs/UserDTO' +// import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' // import { TermsOfUseMother } from '@tests/component/info/models/TermsOfUseMother' // import { JSTermsOfUseMapper } from '@/info/infrastructure/mappers/JSTermsOfUseMapper' -const dataverseInfoRepository: DataverseInfoRepository = {} as DataverseInfoRepository +// const dataverseInfoRepository: DataverseInfoRepository = {} as DataverseInfoRepository const userRepository: UserRepository = {} as UserRepository // TODO - Uncomment when application terms of use are available in API @@ -24,7 +24,7 @@ describe('ValidTokenNotLinkedAccountForm', () => { cy.viewport(1280, 720) // dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves(sanitizedTermsOfUseMock) - dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves('') + // dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves('') userRepository.register = cy.stub().as('registerUser').resolves() }) @@ -114,8 +114,11 @@ describe('ValidTokenNotLinkedAccountForm', () => { ) + cy.findByTestId('termsAcceptedCheckbox').check({ force: true }) + cy.wait(300) + cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') cy.findByRole('button', { name: 'Create Account' }).click() @@ -158,17 +161,23 @@ describe('ValidTokenNotLinkedAccountForm', () => { cy.findByTestId('termsAcceptedCheckbox').check({ force: true }) + cy.wait(300) + cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') // Uncheck and then check again to test validation error from terms not accepted cy.findByTestId('termsAcceptedCheckbox').uncheck({ force: true }) + cy.wait(300) + cy.findByText( 'Please check the box to indicate your acceptance of the General Terms of Use.' ).should('exist') cy.findByTestId('termsAcceptedCheckbox').check({ force: true }) + cy.wait(300) + cy.findByRole('button', { name: 'Create Account' }).should('not.be.disabled') cy.findByRole('button', { name: 'Create Account' }).click() @@ -207,29 +216,6 @@ describe('ValidTokenNotLinkedAccountForm', () => { }) }) - it('shows no terms message when there are no terms of use', () => { - dataverseInfoRepository.getApiTermsOfUse = cy.stub().resolves(null) - - cy.customMount( - {}, - logOut: () => {}, - loginInProgress: false, - tokenData: AuthContextMother.createTokenData(), - idTokenData: AuthContextMother.createTokenData(), - error: null, - login: () => {} // 👈 deprecated - }}> - - - ) - - cy.findByText('There are no Terms of Use for this Dataverse installation.').should('exist') - }) - it('logOut function is called when clicking the Cancel button', () => { const logOut = cy.stub().as('logOut') From f698b578b1fd51c9ac7c36b782814743caf2712c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 4 Dec 2024 22:23:31 -0300 Subject: [PATCH 93/97] test: test only ValidToken form test --- cypress.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress.config.ts b/cypress.config.ts index 3d5be5f45..e1a7020bf 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -21,7 +21,7 @@ export default defineConfig({ }, component: { indexHtmlFile: 'tests/support/component-index.html', - specPattern: ['tests/component/**/*.spec.{js,jsx,ts,tsx}'], + specPattern: ['tests/component/**/ValidTokenNotLinkedAccountForm.spec.{js,jsx,ts,tsx}'], supportFile: 'tests/support/component.ts', fixturesFolder: 'tests/component/fixtures', devServer: { From f12fc28ac1af2884c2de07f863bf21791856f6da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 4 Dec 2024 22:37:38 -0300 Subject: [PATCH 94/97] test: add a wait before checking --- .../ValidTokenNotLinkedAccountForm.spec.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx index 94a2ac35d..c6a240704 100644 --- a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx +++ b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx @@ -115,6 +115,8 @@ describe('ValidTokenNotLinkedAccountForm', () => { ) + cy.wait(300) + cy.findByTestId('termsAcceptedCheckbox').check({ force: true }) cy.wait(300) @@ -159,6 +161,8 @@ describe('ValidTokenNotLinkedAccountForm', () => { // Assert that submit button is disabled if terms are not accepted cy.findByRole('button', { name: 'Create Account' }).should('be.disabled') + cy.wait(300) + cy.findByTestId('termsAcceptedCheckbox').check({ force: true }) cy.wait(300) From db44740250a2e9339cf7b6f3be597514d1757c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 4 Dec 2024 22:49:10 -0300 Subject: [PATCH 95/97] test: wait at the begining helps, removing cypress record config --- .github/workflows/test.yml | 5 ----- cypress.config.ts | 3 +-- .../ValidTokenNotLinkedAccountForm.spec.tsx | 1 - 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 34ae7b0f9..00e81a9fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -122,11 +122,6 @@ jobs: uses: cypress-io/github-action@v5 with: component: true - record: true - env: - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Cypress run Design System uses: cypress-io/github-action@v5 with: diff --git a/cypress.config.ts b/cypress.config.ts index e1a7020bf..630cc27ff 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -3,7 +3,6 @@ import vitePreprocessor from 'cypress-vite' import path from 'path' export default defineConfig({ - projectId: '6sn9cr', video: false, e2e: { baseUrl: 'http://localhost:8000', @@ -21,7 +20,7 @@ export default defineConfig({ }, component: { indexHtmlFile: 'tests/support/component-index.html', - specPattern: ['tests/component/**/ValidTokenNotLinkedAccountForm.spec.{js,jsx,ts,tsx}'], + specPattern: ['tests/component/**/*.spec.{js,jsx,ts,tsx}'], supportFile: 'tests/support/component.ts', fixturesFolder: 'tests/component/fixtures', devServer: { diff --git a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx index c6a240704..6ac2365ac 100644 --- a/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx +++ b/tests/component/sections/sign-up/valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm.spec.tsx @@ -85,7 +85,6 @@ describe('ValidTokenNotLinkedAccountForm', () => { }) }) - // TODO:ME - Remove setting up record of component test and remove secret from github describe('submit form with correct data', () => { it('submits the form with the correct data when tokenData has preferred username, given name, family name and email ', () => { cy.customMount( From 448ed61fecfb5b31799b1eb878e5ca24254501f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 6 Dec 2024 16:49:04 -0300 Subject: [PATCH 96/97] test: initiate api config in tests with bearer token, remove unnecessary api config inits on all integration tests --- .../ApiTokenInfoJSDataverseRepository.spec.ts | 27 --- .../CollectionJSDataverseRepository.spec.ts | 21 -- .../DatasetJSDataverseRepository.spec.ts | 105 +--------- .../files/FileJSDataverseRepository.spec.ts | 196 +----------------- .../integration/files/FileUpload.spec.ts | 27 --- ...DataverseInfoJSDataverseRepository.spec.ts | 11 - ...dataBlockInfoJSDataverseRepository.spec.ts | 11 - .../UserJSDataverseRepository.spec.ts | 11 - tests/e2e-integration/shared/TestsUtils.ts | 10 +- 9 files changed, 15 insertions(+), 404 deletions(-) diff --git a/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts index cbb547f00..f683d28a6 100644 --- a/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts @@ -2,9 +2,6 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { UserJSDataverseRepository } from '../../../../src/users/infrastructure/repositories/UserJSDataverseRepository' import { TestsUtils } from '../../shared/TestsUtils' -import { ApiConfig } from '@iqss/dataverse-client-javascript' -import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' -import { DATAVERSE_BACKEND_URL, OIDC_AUTH_CONFIG } from '@/config' chai.use(chaiAsPromised) const expect = chai.expect @@ -23,26 +20,10 @@ describe('API Token Info JSDataverse Repository', () => { }) it('revoke the API token', async () => { - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - await expect(userRepository.deleteApiToken()).to.be.fulfilled }) it('create or recreate the API token and return the new token info', async () => { - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const recreatedTokenInfo = await userRepository.recreateApiToken() expect(recreatedTokenInfo).to.have.property('apiToken').that.is.a('string') @@ -50,14 +31,6 @@ describe('API Token Info JSDataverse Repository', () => { }) it('fetch the current API token', async () => { - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const tokenInfo = await userRepository.getCurrentApiToken() expect(tokenInfo).to.have.property('apiToken').that.is.a('string') diff --git a/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts index 9fd9ac932..f446ac301 100644 --- a/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts @@ -7,11 +7,6 @@ import { UpwardHierarchyNode } from '../../../../src/shared/hierarchy/domain/models/UpwardHierarchyNode' import { Collection } from '../../../../src/collection/domain/models/Collection' -import { DATAVERSE_BACKEND_URL, OIDC_AUTH_CONFIG } from '@/config' -import { - ApiConfig, - DataverseApiAuthMechanism -} from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' const collectionRepository = new CollectionJSDataverseRepository() const collectionExpected: Collection = { @@ -45,14 +40,6 @@ describe('Collection JSDataverse Repository', () => { it('gets the collection by id', async () => { const collectionResponse = await CollectionHelper.create('new-collection') - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - await collectionRepository.getById(collectionResponse.id).then((collection) => { if (!collection) { throw new Error('Collection not found') @@ -66,14 +53,6 @@ describe('Collection JSDataverse Repository', () => { const uniqueCollectionId = `test-publish-collection-${timestamp}` const collectionResponse = await CollectionHelper.create(uniqueCollectionId) - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - await collectionRepository.publish(collectionResponse.id) await collectionRepository.getById(collectionResponse.id).then((collection) => { if (!collection) { diff --git a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts index 5689d555e..55952eb35 100644 --- a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts @@ -22,9 +22,6 @@ import { DatasetDTO } from '../../../../src/dataset/domain/useCases/DTOs/Dataset import { CollectionHelper } from '../../shared/collection/CollectionHelper' const DRAFT_PARAM = DatasetNonNumericVersion.DRAFT import { VersionUpdateType } from '../../../../src/dataset/domain/models/VersionUpdateType' -import { ApiConfig } from '@iqss/dataverse-client-javascript' -import { DATAVERSE_BACKEND_URL, OIDC_AUTH_CONFIG } from '@/config' -import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' chai.use(chaiAsPromised) const expect = chai.expect @@ -153,15 +150,6 @@ describe('Dataset JSDataverse Repository', () => { it('gets the dataset by persistentId', async () => { const datasetResponse = await DatasetHelper.create(collectionId) - // Change the api config to use bearer token - - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - await datasetRepository .getByPersistentId(datasetResponse.persistentId, DRAFT_PARAM) .then((dataset) => { @@ -193,14 +181,6 @@ describe('Dataset JSDataverse Repository', () => { cy.clearAllLocalStorage() cy.clearAllCookies() - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - await datasetRepository .getByPersistentId(datasetResponse.persistentId, '1.0') .then((dataset) => { @@ -245,15 +225,6 @@ describe('Dataset JSDataverse Repository', () => { await DatasetHelper.publish(datasetResponse.persistentId) await TestsUtils.waitForNoLocks(datasetResponse.persistentId) - // Change the api config to use bearer token - - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - await datasetRepository .getByPersistentId(datasetResponse.persistentId, '1.0') .then((dataset) => { @@ -290,15 +261,6 @@ describe('Dataset JSDataverse Repository', () => { it('gets the dataset by persistentId and version DRAFT keyword', async () => { const datasetResponse = await DatasetHelper.create(collectionId) - // Change the api config to use bearer token - - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - await datasetRepository .getByPersistentId(datasetResponse.persistentId, DRAFT_PARAM) .then((dataset) => { @@ -316,15 +278,6 @@ describe('Dataset JSDataverse Repository', () => { const datasetResponse = await DatasetHelper.create(collectionId) const privateUrlResponse = await DatasetHelper.createPrivateUrl(datasetResponse.id) - // Change the api config to use bearer token - - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - await datasetRepository.getByPrivateUrlToken(privateUrlResponse.token).then((dataset) => { if (!dataset) { throw new Error('Dataset not found') @@ -345,14 +298,6 @@ describe('Dataset JSDataverse Repository', () => { await DatasetHelper.setCitationDateFieldType(datasetResponse.persistentId, 'dateOfDeposit') - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - await datasetRepository .getByPersistentId(datasetResponse.persistentId, '1.0') .then((dataset) => { @@ -374,14 +319,12 @@ describe('Dataset JSDataverse Repository', () => { return DatasetHelper.createAndPublish(previewCollectionId).then((datasetResponse) => { const paginationInfo = new DatasetPaginationInfo(1, 20) - // Change the api config to use bearer token - - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) + // ApiConfig.init( + // `${DATAVERSE_BACKEND_URL}/api/v1`, + // DataverseApiAuthMechanism.BEARER_TOKEN, + // undefined, + // `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + // ) return datasetRepository .getAllWithCount(previewCollectionId, paginationInfo) @@ -404,15 +347,6 @@ describe('Dataset JSDataverse Repository', () => { await DatasetHelper.deaccession(datasetResponse.id) - // Change the api config to use bearer token - - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - await datasetRepository.getByPersistentId(datasetResponse.persistentId).then((dataset) => { if (!dataset) { throw new Error('Dataset not found') @@ -428,15 +362,6 @@ describe('Dataset JSDataverse Repository', () => { const datasetResponse = await DatasetHelper.create(collectionId) await DatasetHelper.lock(datasetResponse.id, DatasetLockReason.FINALIZE_PUBLICATION) - // Change the api config to use bearer token - - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - await datasetRepository .getByPersistentId(datasetResponse.persistentId, DRAFT_PARAM) .then((dataset) => { @@ -457,15 +382,6 @@ describe('Dataset JSDataverse Repository', () => { }) it('creates a new dataset from DatasetDTO', async () => { - // Change the api config to use bearer token - - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const datasetDTO: DatasetDTO = { metadataBlocks: [ { @@ -502,15 +418,6 @@ describe('Dataset JSDataverse Repository', () => { it('publishes a draft dataset', async () => { const datasetResponse = await DatasetHelper.create(collectionId) - // Change the api config to use bearer token - - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - await datasetRepository.publish(datasetResponse.persistentId).then((response) => { expect(response).to.not.exist }) diff --git a/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts index 3a207744b..7926d42d1 100644 --- a/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts @@ -31,9 +31,7 @@ import { } from '../../../../src/dataset/domain/models/Dataset' import { File } from '../../../../src/files/domain/models/File' import { FileIngest, FileIngestStatus } from '../../../../src/files/domain/models/FileIngest' -import { ApiConfig } from '@iqss/dataverse-client-javascript' -import { DATAVERSE_BACKEND_URL, OIDC_AUTH_CONFIG } from '@/config' -import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' + const DRAFT_PARAM = DatasetNonNumericVersion.DRAFT chai.use(chaiAsPromised) @@ -184,14 +182,6 @@ describe('File JSDataverse Repository', () => { it('gets all the files by dataset persistentId with the basic information', async () => { const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(3)) - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DatasetNonNumericVersion.DRAFT @@ -223,14 +213,6 @@ describe('File JSDataverse Repository', () => { } const datasetResponse = await DatasetHelper.createWithFiles([fileData]) - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DatasetNonNumericVersion.DRAFT @@ -238,14 +220,6 @@ describe('File JSDataverse Repository', () => { if (!dataset) throw new Error('Dataset not found') - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - await fileRepository .getAllByDatasetPersistentId(dataset.persistentId, dataset.version) .then((files) => { @@ -256,14 +230,6 @@ describe('File JSDataverse Repository', () => { it('gets all the files by dataset persistentId after dataset publication', async () => { const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(3)) - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DatasetNonNumericVersion.DRAFT @@ -274,14 +240,6 @@ describe('File JSDataverse Repository', () => { await DatasetHelper.publish(dataset.persistentId) await TestsUtils.waitForNoLocks(dataset.persistentId) // Wait for the dataset to be published - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - await fileRepository .getAllByDatasetPersistentId( dataset.persistentId, @@ -312,14 +270,6 @@ describe('File JSDataverse Repository', () => { await DatasetHelper.deaccession(datasetResponse.id) - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId(datasetResponse.persistentId) if (!dataset) throw new Error('Dataset not found') @@ -350,14 +300,6 @@ describe('File JSDataverse Repository', () => { await FileHelper.download(datasetResponse.files[0].id) await TestsUtils.wait(3000) // Wait for the file to be downloaded - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId(datasetResponse.persistentId) if (!dataset) throw new Error('Dataset not found') @@ -379,14 +321,6 @@ describe('File JSDataverse Repository', () => { ] await FileHelper.addLabel(datasetResponse.files[0].id, expectedLabels) - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DatasetNonNumericVersion.DRAFT @@ -408,14 +342,6 @@ describe('File JSDataverse Repository', () => { const expectedLabels = [{ type: FileLabelType.TAG, value: 'Survey' }] await FileHelper.addLabel(datasetResponse.files[0].id, expectedLabels) - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DatasetNonNumericVersion.DRAFT @@ -435,14 +361,6 @@ describe('File JSDataverse Repository', () => { ) if (!datasetResponse.files) throw new Error('Files not found') - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DatasetNonNumericVersion.DRAFT @@ -468,14 +386,6 @@ describe('File JSDataverse Repository', () => { ) await TestsUtils.waitForNoLocks(datasetResponse.persistentId) // Wait for the files to be embargoed - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DRAFT_PARAM @@ -495,14 +405,6 @@ describe('File JSDataverse Repository', () => { const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(1, 'csv')) if (!datasetResponse.files) throw new Error('Files not found') - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DRAFT_PARAM @@ -531,14 +433,6 @@ describe('File JSDataverse Repository', () => { it('gets the files pagination selection when passing pagination', async () => { const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(3)) - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DRAFT_PARAM @@ -563,14 +457,6 @@ describe('File JSDataverse Repository', () => { it('gets all the files by dataset persistentId when passing sortBy criteria', async () => { const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(3)) - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DRAFT_PARAM @@ -601,14 +487,6 @@ describe('File JSDataverse Repository', () => { FileHelper.create('csv') ]) - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DRAFT_PARAM @@ -634,14 +512,6 @@ describe('File JSDataverse Repository', () => { await FileHelper.restrict(datasetResponse.files[0].id) - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DRAFT_PARAM @@ -667,14 +537,6 @@ describe('File JSDataverse Repository', () => { const category = { type: FileLabelType.CATEGORY, value: 'category' } await FileHelper.addLabel(datasetResponse.files[0].id, [category]) - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DRAFT_PARAM @@ -696,14 +558,6 @@ describe('File JSDataverse Repository', () => { it('gets all the files by dataset persistentId when passing searchText criteria', async () => { const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(3)) - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DRAFT_PARAM @@ -737,14 +591,6 @@ describe('File JSDataverse Repository', () => { if (!datasetResponse.files) throw new Error('Files not found') datasetResponse.files.map((file) => FileHelper.delete(file.id)) - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - await fileRepository .getAllByDatasetPersistentId(dataset.persistentId, dataset.version) .then((files) => { @@ -797,14 +643,6 @@ describe('File JSDataverse Repository', () => { }) it('gets FilesCountInfo by dataset persistentId', async () => { - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId(datasetPersistentId, DRAFT_PARAM) if (!dataset) throw new Error('Dataset not found') @@ -882,14 +720,6 @@ describe('File JSDataverse Repository', () => { }) it('gets FilesCountInfo by dataset persistentId when passing filterByType criteria', async () => { - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId(datasetPersistentId, DRAFT_PARAM) if (!dataset) throw new Error('Dataset not found') @@ -983,14 +813,6 @@ describe('File JSDataverse Repository', () => { ] const datasetResponse = await DatasetHelper.createWithFiles(files) - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DRAFT_PARAM @@ -1036,14 +858,6 @@ describe('File JSDataverse Repository', () => { ] const datasetResponse = await DatasetHelper.createWithFiles(files) - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DRAFT_PARAM @@ -1083,14 +897,6 @@ describe('File JSDataverse Repository', () => { const expectedFile = fileExpectedData(datasetResponse.file.id) - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - await fileRepository.getById(datasetResponse.file.id).then((file) => { expect(file.name).to.deep.equal(expectedFile.name) expect(file.ingest).to.deep.equal(expectedFile.ingest) diff --git a/tests/e2e-integration/integration/files/FileUpload.spec.ts b/tests/e2e-integration/integration/files/FileUpload.spec.ts index d671c363f..b2bc1ea9c 100644 --- a/tests/e2e-integration/integration/files/FileUpload.spec.ts +++ b/tests/e2e-integration/integration/files/FileUpload.spec.ts @@ -5,9 +5,6 @@ import { DatasetHelper } from '../../shared/datasets/DatasetHelper' import { FileHelper } from '../../shared/files/FileHelper' import { DatasetNonNumericVersion } from '../../../../src/dataset/domain/models/Dataset' import chaiAsPromised from 'chai-as-promised' -import { ApiConfig } from '@iqss/dataverse-client-javascript' -import { DATAVERSE_BACKEND_URL, OIDC_AUTH_CONFIG } from '@/config' -import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' chai.use(chaiAsPromised) const expect = chai.expect @@ -29,14 +26,6 @@ describe('DirectUpload', () => { it('should upload file and add it to the dataset', async () => { const datasetResponse = await DatasetHelper.create() - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DatasetNonNumericVersion.DRAFT @@ -95,14 +84,6 @@ describe('DirectUpload', () => { let storageId1: string | undefined = undefined let storageId2: string | undefined = undefined - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DatasetNonNumericVersion.DRAFT @@ -184,14 +165,6 @@ describe('DirectUpload', () => { const multipartFile = FileHelper.createMultipartFileBlob() const controller = new AbortController() - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataset = await datasetRepository.getByPersistentId( datasetResponse.persistentId, DatasetNonNumericVersion.DRAFT diff --git a/tests/e2e-integration/integration/info/infrastructure/repositories/DataverseInfoJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/info/infrastructure/repositories/DataverseInfoJSDataverseRepository.spec.ts index 490315e7b..c0828c1df 100644 --- a/tests/e2e-integration/integration/info/infrastructure/repositories/DataverseInfoJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/info/infrastructure/repositories/DataverseInfoJSDataverseRepository.spec.ts @@ -2,9 +2,6 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { DataverseInfoJSDataverseRepository } from '../../../../../../src/info/infrastructure/repositories/DataverseInfoJSDataverseRepository' import { TestsUtils } from '../../../../shared/TestsUtils' -import { ApiConfig } from '@iqss/dataverse-client-javascript' -import { DATAVERSE_BACKEND_URL, OIDC_AUTH_CONFIG } from '@/config' -import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' chai.use(chaiAsPromised) @@ -22,14 +19,6 @@ describe('DataverseInfo JSDataverse Repository', () => { }) it('gets the dataverse version number', async () => { - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const dataverseInfoRepository = new DataverseInfoJSDataverseRepository() const dataverseVersion = await dataverseInfoRepository.getVersion() diff --git a/tests/e2e-integration/integration/metadata-block-info/MetadataBlockInfoJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/metadata-block-info/MetadataBlockInfoJSDataverseRepository.spec.ts index 67533c7bf..6449b90ce 100644 --- a/tests/e2e-integration/integration/metadata-block-info/MetadataBlockInfoJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/metadata-block-info/MetadataBlockInfoJSDataverseRepository.spec.ts @@ -2,9 +2,6 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { MetadataBlockInfoJSDataverseRepository } from '../../../../src/metadata-block-info/infrastructure/repositories/MetadataBlockInfoJSDataverseRepository' import { TestsUtils } from '../../shared/TestsUtils' -import { ApiConfig } from '@iqss/dataverse-client-javascript' -import { DATAVERSE_BACKEND_URL, OIDC_AUTH_CONFIG } from '@/config' -import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' chai.use(chaiAsPromised) const expect = chai.expect @@ -22,14 +19,6 @@ describe('Metadata Block Info JSDataverse Repository', () => { }) it('returns JSON in the correct format', async () => { - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - await metadataBlockInfoRepository.getByName('citation').then((metadataBlockInfo) => { if (!metadataBlockInfo) { throw new Error('Metadata Block Info not found') diff --git a/tests/e2e-integration/integration/users/infrastructure/repositories/UserJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/users/infrastructure/repositories/UserJSDataverseRepository.spec.ts index bdf507bfd..a7da36f7e 100644 --- a/tests/e2e-integration/integration/users/infrastructure/repositories/UserJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/users/infrastructure/repositories/UserJSDataverseRepository.spec.ts @@ -2,9 +2,6 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { UserJSDataverseRepository } from '../../../../../../src/users/infrastructure/repositories/UserJSDataverseRepository' import { TestsUtils } from '../../../../shared/TestsUtils' -import { ApiConfig } from '@iqss/dataverse-client-javascript' -import { DATAVERSE_BACKEND_URL, OIDC_AUTH_CONFIG } from '@/config' -import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' import { User } from '@/users/domain/models/User' chai.use(chaiAsPromised) @@ -23,14 +20,6 @@ describe('User JSDataverse Repository', () => { }) it('gets the authenticated user', async () => { - // Change the api config to use bearer token - ApiConfig.init( - `${DATAVERSE_BACKEND_URL}/api/v1`, - DataverseApiAuthMechanism.BEARER_TOKEN, - undefined, - `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - ) - const expectedUser: Omit = { displayName: 'Dataverse User', firstName: 'Dataverse', diff --git a/tests/e2e-integration/shared/TestsUtils.ts b/tests/e2e-integration/shared/TestsUtils.ts index 2f1e15d9b..9a899f18c 100644 --- a/tests/e2e-integration/shared/TestsUtils.ts +++ b/tests/e2e-integration/shared/TestsUtils.ts @@ -2,7 +2,7 @@ import { ApiConfig } from '@iqss/dataverse-client-javascript/dist/core' import { DataverseApiHelper } from './DataverseApiHelper' import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' import { DatasetHelper } from './datasets/DatasetHelper' -import { DATAVERSE_BACKEND_URL } from '../../../src/config' +import { DATAVERSE_BACKEND_URL, OIDC_AUTH_CONFIG } from '../../../src/config' export class TestsUtils { static readonly DATAVERSE_BACKEND_URL = DATAVERSE_BACKEND_URL @@ -11,7 +11,13 @@ export class TestsUtils { static readonly USER_USERNAME = 'user' static async setup(bearerToken: string) { - ApiConfig.init(`${this.DATAVERSE_BACKEND_URL}/api/v1`, DataverseApiAuthMechanism.API_KEY) + ApiConfig.init( + `${this.DATAVERSE_BACKEND_URL}/api/v1`, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` + ) + await DataverseApiHelper.setup(bearerToken) } From 84f1832bb9544098cb04fae93b6631a5add2d5df Mon Sep 17 00:00:00 2001 From: German Gonzalo Saracca Date: Tue, 10 Dec 2024 16:18:21 +0100 Subject: [PATCH 97/97] Update tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts Co-authored-by: Ellen Kraffmiller --- .../datasets/DatasetJSDataverseRepository.spec.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts index 55952eb35..4fd7ae295 100644 --- a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts @@ -319,13 +319,6 @@ describe('Dataset JSDataverse Repository', () => { return DatasetHelper.createAndPublish(previewCollectionId).then((datasetResponse) => { const paginationInfo = new DatasetPaginationInfo(1, 20) - // ApiConfig.init( - // `${DATAVERSE_BACKEND_URL}/api/v1`, - // DataverseApiAuthMechanism.BEARER_TOKEN, - // undefined, - // `${OIDC_AUTH_CONFIG.LOCAL_STORAGE_KEY_PREFIX}token` - // ) - return datasetRepository .getAllWithCount(previewCollectionId, paginationInfo) .then((datasetsWithCount) => {