diff --git a/.phpunit.cache/test-results b/.phpunit.cache/test-results index 42e148f..3d18c9b 100644 --- a/.phpunit.cache/test-results +++ b/.phpunit.cache/test-results @@ -1 +1 @@ -{"version":"pest_3.7.4","defects":{"P\\Tests\\Requests\\ManualEntries\\AddFileToAccountingEntryLineRequestTest::__pest_evaluable_it_can_perform_the_request":1,"P\\Tests\\Requests\\IbanPayments\\UpdateIbanPaymentRequestTest::__pest_evaluable_it_can_perform_the_request":1,"P\\Tests\\Requests\\Invoices\\CreateAnInvoiceRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\Contacts\\BuilkCreateContactsRequestTest::__pest_evaluable_it_can_perform_the_request":1,"P\\Tests\\Requests\\QrPayments\\UpdateQrPaymentRequestTest::__pest_evaluable_it_can_perform_the_request":1,"P\\Tests\\Requests\\Invoices\\EditAnInvoiceRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\Files\\ShowFileUsageRequestTest::__pest_evaluable_it_can_perform_the_request":1,"P\\Tests\\Requests\\ManualEntries\\CreateManualEntryRequestTest::__pest_evaluable_it_can_perform_the_request":8},"times":{"P\\Tests\\Requests\\Files\\GetAFilePreviewRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\BusinessYears\\FetchAListOfBusinessYearsRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Reports\\JournalRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Payments\\CancelAPaymentTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Titles\\DeleteAnAdditonalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ContactAdditionalAddresses\\SearchContactAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\CalendarYears\\CreateCalendarYearRequestTest::__pest_evaluable_it_can_perform_the_request":0.003,"P\\Tests\\Requests\\Salutations\\SearchSalutationsRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ContactAdditionalAddresses\\FetchAContactAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\IbanPayments\\GetIbanPaymentRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Invoices\\DeleteAnInvoiceRequestTest::__pest_evaluable_it_can_perform_the_request":0.022,"P\\Tests\\Requests\\CalendarYears\\SearchCalendarYearsRequestTest::__pest_evaluable_it_can_perform_the_request":0.002,"P\\Tests\\Requests\\Currencies\\EditACurrencyRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ContactsGroups\\EditAContactGroupRequestTest::__pest_evaluable_it_can_perform_the_request":0.006,"P\\Tests\\Requests\\ContactsGroups\\CreateContactGroupRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Salutations\\FetchASalutationRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Files\\CreateAFileRequestTest::__pest_evaluable_it_can_perform_the_request":0.002,"P\\Tests\\Requests\\Units\\FetchAListOfUnitsRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Payments\\FetchAListOfPaymentsTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Currencies\\CreateCurrencyRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Notes\\FetchAListOfNotesRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ContactsSectors\\SearchContactSectorsRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Contacts\\EditAContactRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ContactsGroups\\SearchContactGroupsRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\QrPayments\\CreateQrPaymentRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ContactAdditionalAddresses\\CreateContactAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ContactAdditionalAddresses\\EditAContactAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\BusinessYears\\FetchABusinessYearRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ContactsSectors\\FetchAListOfContactSectorsRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ManualEntries\\AddFileToAccountingEntryLineRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ManualEntries\\DeleteFileOfAccountingEntryLineRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\IbanPayments\\UpdateIbanPaymentRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Currencies\\FetchExchangeRatesForCurrenciesRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\DocumentSettings\\FetchAListOfDocumentSettingsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Titles\\EditAnAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\AdditionalAddresses\\FetchAListOfAdditionalAddressesRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Notes\\CreateANoteRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\CalendarYears\\FetchACalendarYearRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Invoices\\CreateAnInvoiceRequestTest::__pest_evaluable_it_can_perform_the_request":0.004,"P\\Tests\\Requests\\ContactsRelations\\DeleteAContactRelationRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\PaymentTypes\\FetchAListOfPaymentTypesRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ManualEntries\\FetchAListOfManualEntriesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Contacts\\BuilkCreateContactsRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\AccountGroups\\FetchAListOfAccountGroupsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Core\\ArchTest::__pest_evaluable_it_will_not_use_any_debug_function":0.204,"P\\Tests\\Requests\\ContactsRelations\\EditAContactRelationRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Currencies\\DeleteACurrencyRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\QrPayments\\UpdateQrPaymentRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ContactsRelations\\FetchAContactRelationRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ContactsGroups\\FetchAListOfContactGroupsRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Invoices\\SubPositions\\CreateASubPositionRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Taxes\\FetchAListOfTaxesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\BankAccounts\\FetchASingleBankAccountRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ContactAdditionalAddresses\\DeleteAContactAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\BankAccounts\\FetchAListOfBankAccountsRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Invoices\\EditAnInvoiceRequestTest::__pest_evaluable_it_can_perform_the_request":0.006,"P\\Tests\\Requests\\Contacts\\RestoreAContactRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\AdditionalAddresses\\DeleteAnAdditonalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Files\\ShowFileUsageRequestTest::__pest_evaluable_it_can_perform_the_request":0.003,"P\\Tests\\Requests\\Invoices\\DefaultPositions\\CreateADefaultPositionRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\CompanyProfiles\\FetchAListOfCompanyProfilesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Titles\\SearchTitlesRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Contacts\\FetchAContactRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Files\\SearchFilesRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\AdditionalAddresses\\CreateAnAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Notes\\FetchANoteRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Invoices\\FetchAListOfInvoicesRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Titles\\FetchAListOfTitlesRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ManualEntries\\CreateManualEntryRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Salutations\\EditASalutationRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Users\\FetchAListOfUsersRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Accounts\\SearchAccountsRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Invoices\\ShowPdfRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Files\\FetchAListOfFilesRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Salutations\\FetchAListOfSalutationsRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Currencies\\FetchAllPossibleCurrencyCodesRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Files\\DeleteAFileRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Invoices\\FetchAnInvoiceRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Payments\\DeleteAPaymentTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Files\\EditAFileRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Taxes\\FetchATaxRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\AdditionalAddresses\\FetchAnAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Invoices\\CancelAnInvoiceRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Projects\\FetchAListOfPaymentTypesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Titles\\FetchATitleRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Files\\GetASingleFileRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ContactsGroups\\FetchAContactGroupRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ContactAdditionalAddresses\\FetchAListOfContactAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Contacts\\CreateContactRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ContactsRelations\\SearchContactRelationsRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\CalendarYears\\FetchAListOfCalendarYearsRequestTest::__pest_evaluable_it_can_perform_the_request":0.002,"P\\Tests\\Requests\\Notes\\DeleteANoteRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\QrPayments\\GetQrPaymentRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\IbanPayments\\CreateIbanPaymentRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\AdditionalAddresses\\SearchAdditionalAddressesRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ContactsGroups\\DeleteAContactGroupRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Files\\DownloadFIleRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Accounts\\FetchAListOfAccountsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Contacts\\SearchContactsRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ManualEntries\\FetchFileOfAccountingEntryLineRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ManualEntries\\FetchFilesOfAccountingEntryRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\AdditionalAddresses\\EditAnAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Taxes\\DeleteATaxRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Notes\\SearchNotesRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Notes\\EditANoteRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Currencies\\FetchACurrencyRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ContactsRelations\\FetchAListOfContactRelationsRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Currencies\\FetchAListOfCurrenciesRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Salutations\\CreateASalutationRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\BusinessActivities\\FetchAListOfBusinessActivitesRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Languages\\FetchAListOfLanguagesRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Titles\\CreateASalutationRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Contacts\\FetchAListOfContactsRequestTest::__pest_evaluable_it_can_perform_the_request":0.005,"P\\Tests\\Requests\\Salutations\\DeleteASalutationRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\CompanyProfiles\\FetchACompanyProfileRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Contacts\\DeleteAContactRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ManualEntries\\GetNextReferenceNumberRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Users\\FetchAuthenticatedUserRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ContactsRelations\\CreateContactRelationRequestTest::__pest_evaluable_it_can_perform_the_request":0}} \ No newline at end of file +{"version":"pest_3.8.2","defects":{"P\\Tests\\Requests\\Contacts\\RestoreAContactRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\Files\\GetAFilePreviewRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\ContactAdditionalAddresses\\DeleteAContactAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\ManualEntries\\CreateManualEntryRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\OpenID\\FetchUserinfoRequestTest::__pest_evaluable_it_can_perform_the_userinfo_request":7,"P\\Tests\\Requests\\Files\\EditAFileRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\Notes\\EditANoteRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\ContactAdditionalAddresses\\FetchAContactAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\Files\\DeleteAFileRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\ContactsGroups\\FetchAContactGroupRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\Contacts\\EditAContactRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\Files\\DownloadFIleRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\IbanPayments\\UpdateIbanPaymentRequestTest::__pest_evaluable_it_can_perform_the_request":1,"P\\Tests\\Requests\\Files\\ShowFileUsageRequestTest::__pest_evaluable_it_can_perform_the_request":1,"P\\Tests\\Requests\\Notes\\DeleteANoteRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\Currencies\\EditACurrencyRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\AdditionalAddresses\\EditAnAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\AdditionalAddresses\\FetchAnAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\Contacts\\DeleteAContactRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\Invoices\\DeleteAnInvoiceRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\ManualEntries\\DeleteFileOfAccountingEntryLineRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\Contacts\\BuilkCreateContactsRequestTest::__pest_evaluable_it_can_perform_the_request":1,"P\\Tests\\Requests\\Salutations\\EditASalutationRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\ContactAdditionalAddresses\\EditAContactAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\AdditionalAddresses\\DeleteAnAdditonalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\Files\\GetASingleFileRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\ContactsRelations\\EditAContactRelationRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\ManualEntries\\AddFileToAccountingEntryLineRequestTest::__pest_evaluable_it_can_perform_the_request":1,"P\\Tests\\Requests\\ContactsRelations\\FetchAContactRelationRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\ManualEntries\\FetchFileOfAccountingEntryLineRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\ContactsGroups\\DeleteAContactGroupRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\ContactsRelations\\DeleteAContactRelationRequestTest::__pest_evaluable_it_can_perform_the_request":8,"P\\Tests\\Requests\\QrPayments\\UpdateQrPaymentRequestTest::__pest_evaluable_it_can_perform_the_request":1,"P\\Tests\\Services\\BexioOAuthServiceTest::__pest_evaluable__BexioOAuthService__\u2192_it_refreshes_and_persists_authenticator":8,"P\\Tests\\Services\\BexioOAuthServiceTest::__pest_evaluable__BexioOAuthService__\u2192_it_can_exchange_code_for_authenticator":8,"P\\Tests\\Services\\BexioOAuthServiceTest::__pest_evaluable_it_fetches_userinfo_using_MockClient":8,"P\\Tests\\Services\\BexioOAuthServiceTest::__pest_evaluable__BexioOAuthService__\u2192_it_throws_on_token_exchange_failure":8,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_shows_error_on_invalid_callback":7,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_shows_error_view_on_OAuth_config_error_during_redirect":7,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_shows_error_view_on_API_error_during_redirect":8,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_handles_userinfo_verification_failure":8,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_handles_missing_code_state_in_callback":8,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_handles_token_exchange_failure":8,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_redirects_to_Bexio_authorization_page_successfully":7,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_shows_error_view_on_unexpected_error_during_redirect":7,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_stores_authenticator_and_shows_success":7,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_shows_error_view_on_unauthorized_error_during_redirect":8,"P\\Tests\\Support\\BexioTokenStoreTest::__pest_evaluable_it_overwrites_the_authenticator":7,"P\\Tests\\Support\\BexioTokenStoreTest::__pest_evaluable_it_stores_and_retrieves_authenticator":7,"P\\Tests\\Support\\BexioTokenStoreTest::__pest_evaluable_it_stores_authenticator_with_ttl":7,"P\\Tests\\Support\\BexioTokenStoreTest::__pest_evaluable_it_forgets_the_authenticator":8,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_shows_cancellation_view_when_user_rejects_authorization":7},"times":{"P\\Tests\\Requests\\Contacts\\RestoreAContactRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Notes\\FetchANoteRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\CompanyProfiles\\FetchACompanyProfileRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Titles\\FetchATitleRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ManualEntries\\FetchFilesOfAccountingEntryRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Files\\GetAFilePreviewRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\QrPayments\\CreateQrPaymentRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ContactAdditionalAddresses\\DeleteAContactAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ManualEntries\\CreateManualEntryRequestTest::__pest_evaluable_it_can_perform_the_request":0.002,"P\\Tests\\Requests\\ContactsSectors\\FetchAListOfContactSectorsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Currencies\\FetchAListOfCurrenciesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Currencies\\FetchExchangeRatesForCurrenciesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ContactsGroups\\SearchContactGroupsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Salutations\\CreateASalutationRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Titles\\SearchTitlesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Invoices\\CancelAnInvoiceRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Files\\FetchAListOfFilesRequestTest::__pest_evaluable_it_can_perform_the_request":0.002,"P\\Tests\\Requests\\OpenID\\FetchUserinfoRequestTest::__pest_evaluable_it_can_perform_the_userinfo_request":0.001,"P\\Tests\\Requests\\Contacts\\CreateContactRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Reports\\JournalRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Files\\EditAFileRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\IbanPayments\\CreateIbanPaymentRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\AdditionalAddresses\\SearchAdditionalAddressesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Projects\\FetchAListOfPaymentTypesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Payments\\DeleteAPaymentTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Notes\\EditANoteRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Invoices\\DefaultPositions\\CreateADefaultPositionRequestTest::__pest_evaluable_it_can_perform_the_request":0.002,"P\\Tests\\Requests\\ContactAdditionalAddresses\\FetchAContactAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Files\\DeleteAFileRequestTest::__pest_evaluable_it_can_perform_the_request":0.006,"P\\Tests\\Requests\\Units\\FetchAListOfUnitsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ContactsGroups\\FetchAListOfContactGroupsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ContactsGroups\\FetchAContactGroupRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Contacts\\EditAContactRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Titles\\CreateASalutationRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Notes\\FetchAListOfNotesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Files\\DownloadFIleRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Titles\\EditAnAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\IbanPayments\\UpdateIbanPaymentRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\BankAccounts\\FetchASingleBankAccountRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Core\\ArchTest::__pest_evaluable_it_will_not_use_any_debug_function":0.274,"P\\Tests\\Requests\\Files\\ShowFileUsageRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\BusinessYears\\FetchAListOfBusinessYearsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Invoices\\SubPositions\\CreateASubPositionRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Salutations\\FetchASalutationRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Notes\\DeleteANoteRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Payments\\CancelAPaymentTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\CalendarYears\\SearchCalendarYearsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Notes\\CreateANoteRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ContactsGroups\\EditAContactGroupRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\BusinessActivities\\FetchAListOfBusinessActivitesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\QrPayments\\GetQrPaymentRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ContactsGroups\\CreateContactGroupRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Titles\\FetchAListOfTitlesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\BankAccounts\\FetchAListOfBankAccountsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\AccountGroups\\FetchAListOfAccountGroupsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Accounts\\SearchAccountsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Salutations\\SearchSalutationsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Currencies\\FetchAllPossibleCurrencyCodesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\CalendarYears\\FetchACalendarYearRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Taxes\\FetchATaxRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Currencies\\EditACurrencyRequestTest::__pest_evaluable_it_can_perform_the_request":0.002,"P\\Tests\\Requests\\AdditionalAddresses\\EditAnAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ManualEntries\\FetchAListOfManualEntriesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\AdditionalAddresses\\CreateAnAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\AdditionalAddresses\\FetchAnAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Invoices\\ShowPdfRequestTest::__pest_evaluable_it_can_perform_the_request":0.004,"P\\Tests\\Requests\\AdditionalAddresses\\FetchAListOfAdditionalAddressesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Salutations\\DeleteASalutationRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Contacts\\DeleteAContactRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Currencies\\FetchACurrencyRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Files\\SearchFilesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Invoices\\DeleteAnInvoiceRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Contacts\\FetchAContactRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Accounts\\FetchAListOfAccountsRequestTest::__pest_evaluable_it_can_perform_the_request":0.002,"P\\Tests\\Requests\\ManualEntries\\DeleteFileOfAccountingEntryLineRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Contacts\\BuilkCreateContactsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Payments\\FetchAListOfPaymentsTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Titles\\DeleteAnAdditonalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Notes\\SearchNotesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Invoices\\CreateAnInvoiceRequestTest::__pest_evaluable_it_can_perform_the_request":0.014,"P\\Tests\\Requests\\Salutations\\EditASalutationRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Currencies\\DeleteACurrencyRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Currencies\\CreateCurrencyRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ContactAdditionalAddresses\\EditAContactAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0.058,"P\\Tests\\Requests\\ContactAdditionalAddresses\\SearchContactAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\AdditionalAddresses\\DeleteAnAdditonalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Files\\GetASingleFileRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\BusinessYears\\FetchABusinessYearRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Contacts\\FetchAListOfContactsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Taxes\\FetchAListOfTaxesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ContactsRelations\\EditAContactRelationRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ContactAdditionalAddresses\\FetchAListOfContactAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Invoices\\EditAnInvoiceRequestTest::__pest_evaluable_it_can_perform_the_request":0.002,"P\\Tests\\Requests\\CalendarYears\\FetchAListOfCalendarYearsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\CalendarYears\\CreateCalendarYearRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\CompanyProfiles\\FetchAListOfCompanyProfilesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\IbanPayments\\GetIbanPaymentRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\DocumentSettings\\FetchAListOfDocumentSettingsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ContactsRelations\\FetchAListOfContactRelationsRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ManualEntries\\AddFileToAccountingEntryLineRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ContactsRelations\\FetchAContactRelationRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ContactAdditionalAddresses\\CreateContactAdditionalAddressRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ContactsRelations\\SearchContactRelationsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Taxes\\DeleteATaxRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Salutations\\FetchAListOfSalutationsRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\Users\\FetchAuthenticatedUserRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ManualEntries\\GetNextReferenceNumberRequestTest::__pest_evaluable_it_can_perform_the_request":0.002,"P\\Tests\\Requests\\Contacts\\SearchContactsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ManualEntries\\FetchFileOfAccountingEntryLineRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ContactsRelations\\CreateContactRelationRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ContactsGroups\\DeleteAContactGroupRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\ContactsSectors\\SearchContactSectorsRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Users\\FetchAListOfUsersRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Languages\\FetchAListOfLanguagesRequestTest::__pest_evaluable_it_can_perform_the_request":0,"P\\Tests\\Requests\\ContactsRelations\\DeleteAContactRelationRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Invoices\\FetchAnInvoiceRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Files\\CreateAFileRequestTest::__pest_evaluable_it_can_perform_the_request":0.002,"P\\Tests\\Requests\\QrPayments\\UpdateQrPaymentRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\Invoices\\FetchAListOfInvoicesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Requests\\PaymentTypes\\FetchAListOfPaymentTypesRequestTest::__pest_evaluable_it_can_perform_the_request":0.001,"P\\Tests\\Services\\BexioOAuthServiceTest::__pest_evaluable__BexioOAuthService__\u2192_it_verifyUserinfo_throws_on_unverified_email":0,"P\\Tests\\Services\\BexioOAuthServiceTest::__pest_evaluable__BexioOAuthService__\u2192_it_returns_null_if_no_authenticator_to_refresh":0,"P\\Tests\\Services\\BexioOAuthServiceTest::__pest_evaluable__BexioOAuthService__\u2192_it_verifyUserinfo_throws_on_wrong_email":0,"P\\Tests\\Services\\BexioOAuthServiceTest::__pest_evaluable__BexioOAuthService__\u2192_it_refreshes_and_persists_authenticator":0,"P\\Tests\\Services\\BexioOAuthServiceTest::__pest_evaluable__BexioOAuthService__\u2192_it_can_exchange_code_for_authenticator":0.001,"P\\Tests\\Services\\BexioOAuthServiceTest::__pest_evaluable_it_fetches_userinfo_using_MockClient":0.001,"P\\Tests\\Services\\BexioOAuthServiceTest::__pest_evaluable__BexioOAuthService__\u2192_it_verifyUserinfo_passes_with_correct_data":0,"P\\Tests\\Services\\BexioOAuthServiceTest::__pest_evaluable__BexioOAuthService__\u2192_it_throws_on_token_exchange_failure":0,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_shows_error_on_invalid_callback":0.002,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_shows_error_view_on_OAuth_config_error_during_redirect":0.001,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_shows_error_view_on_API_error_during_redirect":0.001,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_handles_userinfo_verification_failure":0.001,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_handles_missing_code_state_in_callback":0.001,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_handles_token_exchange_failure":0.001,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_redirects_to_Bexio_authorization_page_successfully":0.005,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_shows_error_view_on_unexpected_error_during_redirect":0.001,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_stores_authenticator_and_shows_success":0.003,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_shows_error_view_on_unauthorized_error_during_redirect":0.038,"P\\Tests\\Support\\BexioTokenStoreTest::__pest_evaluable_it_overwrites_the_authenticator":0.001,"P\\Tests\\Support\\BexioTokenStoreTest::__pest_evaluable_it_stores_and_retrieves_authenticator":0,"P\\Tests\\Support\\BexioTokenStoreTest::__pest_evaluable_it_returns_null_if_cache_is_empty":0,"P\\Tests\\Support\\BexioTokenStoreTest::__pest_evaluable_it_stores_authenticator_with_ttl":2.006,"P\\Tests\\Support\\BexioTokenStoreTest::__pest_evaluable_it_forgets_the_authenticator":0,"P\\Tests\\Controllers\\BexioOAuthControllerTest::__pest_evaluable_it_shows_cancellation_view_when_user_rejects_authorization":0.001}} \ No newline at end of file diff --git a/README.md b/README.md index 081a1f9..60d78e4 100644 --- a/README.md +++ b/README.md @@ -15,20 +15,20 @@ Bexio is a cloud-based simple business software for the self-employed, small bus ## πŸ›  Requirements -| Package | PHP | Laravel | -|-----------|-------------|-----------| -| v12.0.0 | ^8.2 - ^8.4 | 12.x | -| v11.0.0 | ^8.2 - ^8.3 | 11.x | -| v1.0.0 | ^8.2 | 10.x | +| Package | PHP | Laravel | +| ------- | ----------- | ------- | +| v12.0.0 | ^8.2 - ^8.4 | 12.x | +| v11.0.0 | ^8.2 - ^8.3 | 11.x | +| v1.0.0 | ^8.2 | 10.x | ## Authentication The currently supported authentication methods are: -| Method | Supported | -|-----------|:-----------:| -| API token | βœ… | -| OAuth | ❌ | +| Method | Supported | +| --------- | :-------: | +| API token | βœ… | +| OAuth | βœ… | ## βš™οΈ Installation @@ -38,20 +38,181 @@ You can install the package via composer: composer require codebar-ag/laravel-bexio ``` -Optionally, you can publish the config file with: +### πŸ”§ Configuration + +Publish the config file to customize authentication settings: + +```bash +php artisan vendor:publish --provider="CodebarAg\Bexio\BexioServiceProvider" --tag=bexio-config +``` + +Optionally, you may also publish the controller and views: ```bash -php artisan vendor:publish --provider="CodebarAg\Bexio\BexioServiceProvider" --tag="bexio-config" +php artisan vendor:publish --provider="CodebarAg\Bexio\BexioServiceProvider" --tag=bexio-controller +php artisan vendor:publish --provider="CodebarAg\Bexio\BexioServiceProvider" --tag=bexio-views ``` -You can add the following env variables to your `.env` file: +Add the following variables to your `.env` file as needed: ```dotenv -BEXIO_API_TOKEN= # Your Bexio API token +BEXIO_API_TOKEN= # Your Bexio API token (for PAT) +BEXIO_USE_OAUTH2=true # Set to true to use OAuth2 (default: false) +BEXIO_OAUTH2_CLIENT_ID= # Your Bexio Client ID (for OAuth2) +BEXIO_OAUTH2_CLIENT_SECRET= # Your Bexio Client Secret (for OAuth2) +BEXIO_OAUTH2_EMAIL= # The email address for the Bexio account that is used to authorize the application (for OAuth2) +``` + +> **Note:** +> You only need to set either `BEXIO_API_TOKEN` (for Personal Access Token authentication) **or** the OAuth2 environment variables (`BEXIO_OAUTH2_CLIENT_ID`, `BEXIO_OAUTH2_CLIENT_SECRET`, `BEXIO_OAUTH2_EMAIL`, etc.)β€”not both. +> +> - If `BEXIO_USE_OAUTH2=true`, the package will use OAuth2 and ignore `BEXIO_API_TOKEN`. +> - If `BEXIO_USE_OAUTH2=false` (or unset), the package will use the API token and ignore the OAuth2 environment variables. + +You can create and retrieve either: + +- An **API Token** (Personal Access Token), or +- A **Client ID / Client Secret** for OAuth2 + +from your [Bexio Developer Dashboard](https://developer.bexio.com). + +## πŸ” OAuth2 + +To use OAuth2, set `BEXIO_USE_OAUTH2=true` and ensure all relevant environment variables are configured. + +### OAuth2 Flow + +1. User visits `/bexio/oauth/redirect` to start the flow. +2. After authenticating with Bexio, the user is redirected to `/bexio/oauth/callback`. +3. The callback handler exchanges the authorization code for an access and refresh token. +4. Tokens are securely stored in cache and used for subsequent API requests. + +> ⚠️ **Refresh Token Expiry Notice** +> Refresh tokens do not have a fixed expiration, but they are tied to an _offline session_ that expires after **1 year of inactivity**. +> You must refresh the token at least once a year to avoid requiring reauthorization. + +πŸ“– For details, see the [Bexio API Docs on Authorization](https://docs.bexio.com/#section/Authentication). + +--- + +### πŸ›‚ Scopes + +When using OAuth2, you **must explicitly request the scopes** you need. These control which API endpoints your access token can use. + +Define scopes in your `config/bexio.php`: + +```php +'auth' => [ + 'scopes' => ['contact_edit', 'kb_invoice_show'], +], +``` + +> ℹ️ Some scopes imply others. For example, `contact_edit` also grants `contact_show`. + +πŸ”— [See the full list of scopes in the Bexio API Docs](https://docs.bexio.com/#section/Authentication/API-Scopes) + +--- + +### βœ… Required Scopes for Package + +These OpenID Connect scopes are always applied by the package: + +- `openid` +- `offline_access` +- `email` + +These are required to: + +- Verify the authorized email from Bexio +- Enable token refresh +- Retrieve identity claims + +--- + +### 🚦 OAuth2 Routes + +| Route | Description | +| --------------------- | -------------------------- | +| /bexio/oauth/redirect | Start OAuth2 authorization | +| /bexio/oauth/callback | Handle OAuth2 callback | + +**Note:** The `route_prefix` config option allows you to change the base URI for all Bexio package routes (default is `/bexio`). +For most applications, you should leave this setting as-is. Only change it if you need to avoid a route conflict or require a custom URL structure. + +--- + +### πŸ’Ύ Token Storage + +OAuth2 tokens are cached (encrypted) via the `BexioOAuthTokenStore` class, which uses Laravel's cache and encryption facilities by default. + +You may override this class if you wish to store tokens in a database, Redis, or another driver, or to customize encryption and retrieval logic. + +--- + +### 🧰 Full Configuration Example + +After publishing the config file, you can customize values in `config/bexio.php`: + +```php +return [ + 'auth' => [ + 'use_oauth2' => env('BEXIO_USE_OAUTH2', false), + 'token' => env('BEXIO_API_TOKEN'), + 'client_id' => env('BEXIO_OAUTH2_CLIENT_ID'), + 'client_secret' => env('BEXIO_OAUTH2_CLIENT_SECRET'), + 'oauth_email' => env('BEXIO_OAUTH2_EMAIL'), + 'scopes' => [], + ], + 'route_prefix' => 'bexio', +]; ``` -You can retrieve your API token from -your [Bexio Dashboard](https://office.bexio.com/index.php/admin/apiTokens) +--- + +### πŸ”„ Migrating from PAT to OAuth2 + +To switch from a Personal Access Token (PAT) to OAuth2 authentication: + +1. **Update your `.env` file:** + + ```dotenv + BEXIO_USE_OAUTH2=true + BEXIO_OAUTH2_CLIENT_ID=your-client-id + BEXIO_OAUTH2_CLIENT_SECRET=your-client-secret + BEXIO_OAUTH2_EMAIL=your-verified-bexio-email + ``` + + > ℹ️ You can leave `BEXIO_API_TOKEN` blank or remove it. Only one method needs to be active β€” if `BEXIO_USE_OAUTH2=true` and `BEXIO_API_TOKEN` is set, it will be ignored. + +2. **Publish config (if not already done):** + + ```bash + php artisan vendor:publish --tag=bexio-config + ``` + +3. **Configure OAuth2 scopes:** + + Define the scopes your app needs in `config/bexio.php`. + + See [Scopes](#πŸ›‚-scopes) for details and examples. + +4. **(Optional) Customize the controller or views:** + + ```bash + php artisan vendor:publish --tag=bexio-controller + php artisan vendor:publish --tag=bexio-views + ``` + +5. **Clear configuration and cache:** + + ```bash + php artisan config:clear + php artisan cache:clear + ``` + +6. **Start the OAuth2 flow:** + + Visit `/bexio/oauth/redirect` in your browser to authorize the app and store tokens. ## Usage @@ -62,19 +223,19 @@ use CodebarAg\Bexio\BexioConnector; ... $connector = new BexioConnector(); -```` +``` ### Responses The following responses are currently supported for retrieving the response body: -| Response Methods | Description | Supported | -|-------------------|------------------------------------------------------------------------------------------------------------------------------------|:-----------:| -| body | Returns the HTTP body as a string | βœ… | -| json | Retrieves a JSON response body and json_decodes it into an array. | βœ… | -| object | Retrieves a JSON response body and json_decodes it into an object. | βœ… | -| collect | Retrieves a JSON response body and json_decodes it into a Laravel collection. **Requires illuminate/collections to be installed.** | βœ… | -| dto | Converts the response into a data-transfer object. You must define your DTO first | βœ… | +| Response Methods | Description | Supported | +| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- | :-------: | +| body | Returns the HTTP body as a string | βœ… | +| json | Retrieves a JSON response body and json_decodes it into an array. | βœ… | +| object | Retrieves a JSON response body and json_decodes it into an object. | βœ… | +| collect | Retrieves a JSON response body and json_decodes it into a Laravel collection. **Requires illuminate/collections to be installed.** | βœ… | +| dto | Converts the response into a data-transfer object. You must define your DTO first | βœ… | See https://docs.saloon.dev/the-basics/responses for more information. @@ -82,8 +243,8 @@ See https://docs.saloon.dev/the-basics/responses for more information. We provide enums for the following values: -| Enum | Values | -|----------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Enum | Values | +| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Accounts: SearchFieldEnum | ACCOUNT_NO(), self FIBU_ACCOUNT_GROUP_ID(), NAME(), ACCOUNT_TYPE() | | Accounts: AccountTypeEnum | EARNINGS(), EXPENDITURES(), ACTIVE_ACCOUNTS(), PASSIVE_ACCOUNTS(), COMPLETE_ACCOUNTS() | | AdditionalAddresses: AddSearchTypeEnum | ID(), ID_ASC(), ID_DESC(), NAME(), NAME_ASC(), NAME_DESC() | @@ -103,65 +264,63 @@ We provide enums for the following values: | Titles: OrderByEnum | ID(), ID_ASC(), ID_DESC(), NAME(), NAME_ASC(), NAME_DESC() | | SearchCriteriaEnum | EQUALS(), DOUBLE_EQUALS(), EQUAL(), NOT_EQUALS(), GREATER_THAN_SYMBOL(), GREATER_THAN(), GREATER_EQUAL_SYMBOL(), GREATER_EQUAL(), LESS_THAN_SYMBOL(), LESS_THAN(), LESS_EQUAL_SYMBOL(), LESS_EQUAL(), LIKE(), NOT_LIKE(), IS_NULL(), NOT_NULL(), IN(), NOT_IN() | - - - `Note: When using the dto method on a response, the enum values will be converted to their respective enum class.` ### DTOs We provide DTOs for the following: -| DTO | -|---------------------------------------| -| AccountGroupDTO | -| AccountDTO | -| BankAccountDTO | -| AdditionalAddressDTO | -| BankAccountDTO | -| BusinessActivityDTO | -| BusinessYearDTO | -| CalendarYearDTO | -| CompanyProfileDTO | -| ContactAdditionalAddressDTO | -| ContactGroupDTO | -| ContactRelationDTO | -| ContactDTO | -| CreateEditContactDTO | -| ContactSectorDTO | -| CurrencyDTO | -| CreateCurrencyDTO | -| EditCurrencyDTO | -| ExchangeCurrencyDTO | -| DocumentSettingDTO | -| FileDTO | -| EditFileDTO | -| FileUsageDTO | -| InvoiceDTO | -| InvoicePositionDTO | -| InvoiceTaxDTO | -| PdfDTO | -| LanguageDTO | -| AddFileDTO | -| EntryDTO | -| FileDTO | -| ManualEntryDTO | -| NoteDTO | -| PaymentDTO | -| PaymentTypeDTO | -| ProjectDTO | -| JournalDTO | -| SalutationDTO | -| TaxDTO | -| TitleDTO | -| UnitDTO | -| UserDTO | -| VatPeriodDTO | +| DTO | +| --------------------------- | +| AccountGroupDTO | +| AccountDTO | +| BankAccountDTO | +| AdditionalAddressDTO | +| BankAccountDTO | +| BusinessActivityDTO | +| BusinessYearDTO | +| CalendarYearDTO | +| CompanyProfileDTO | +| ContactAdditionalAddressDTO | +| ContactGroupDTO | +| ContactRelationDTO | +| ContactDTO | +| CreateEditContactDTO | +| ContactSectorDTO | +| CurrencyDTO | +| CreateCurrencyDTO | +| EditCurrencyDTO | +| ExchangeCurrencyDTO | +| DocumentSettingDTO | +| FileDTO | +| EditFileDTO | +| FileUsageDTO | +| InvoiceDTO | +| InvoicePositionDTO | +| InvoiceTaxDTO | +| PdfDTO | +| LanguageDTO | +| AddFileDTO | +| EntryDTO | +| FileDTO | +| ManualEntryDTO | +| NoteDTO | +| PaymentDTO | +| PaymentTypeDTO | +| ProjectDTO | +| JournalDTO | +| SalutationDTO | +| TaxDTO | +| TitleDTO | +| UnitDTO | +| UserDTO | +| UserinfoDTO | +| VatPeriodDTO | In addition to the above, we also provide DTOs to be used for create and edit request for the following: -| DTO | -|---------------------------------------| +| DTO | +| ------------------------------------- | | CreateCalendarYearDTO | | CreateEditAdditionalAddressDTO | | CreateEditContactAdditionalAddressDTO | @@ -187,18 +346,29 @@ In addition to the above, we also provide DTOs to be used for create and edit re ```php use CodebarAg\bexio\BexioConnector; +// === PAT (Personal Access Token) Authentication === // You can either set the token in the constructor or in the .env file // PROVIDE TOKEN IN CONSTRUCTOR $connector = new BexioConnector(token: 'your-token'); - + // OR - + // PROVIDE TOKEN IN .ENV FILE $connector = new BexioConnector(); + +// === OAuth2 Authentication === +// If you have configured OAuth2 in your .env and config/bexio.php, +// the connector will automatically use the cached OAuth2 token +// after completing the authorization flow via the provided routes. + +// Example (after OAuth2 flow is complete): +$connector = new BexioConnector(); +// No token parameter needed; uses OAuth2 credentials from cache ``` ### Accounts + ```php /** * Fetch A List Of Account Groups @@ -224,6 +394,7 @@ $accounts = $connector->send(new SearchAccountsRequest( ``` ### Addresses + ```php /** * Fetch A List Of Addresses @@ -262,7 +433,7 @@ $address = $connector->send(new CreateAddressRequest( address: 'Test Address', postcode: '1234', city: 'Test City', - ) + ) )); ``` @@ -279,7 +450,7 @@ $address = $connector->send(new EditAnAddressRequest( address: 'Test Address Edit', postcode: '4567', city: 'Test City Edit', - ) + ) )); ``` @@ -292,8 +463,8 @@ $address = $connector->send(new DeleteAnAddressRequest( )); ``` - ### Bank Accounts + ```php /** * Fetch A List Of Bank Accounts @@ -310,8 +481,8 @@ $bankAccount = $connector->send(new FetchASingleBankAccountRequest( ))->dto(); ``` - ### Business Years + ```php /** * Fetch A List Of Business Years @@ -329,6 +500,7 @@ $businessYear = $connector->send(new FetchABusinessYearRequest( ``` ### Calendar Years + ```php /** * Fetch A List Of Calendar Years @@ -346,6 +518,7 @@ $calendarYear = $connector->send(new FetchACalendarYearRequest( ``` ### Company Profiles + ```php /** * Fetch A List Of Company Profiles @@ -363,6 +536,7 @@ $companyProfile = $connector->send(new FetchACompanyProfileRequest( ``` ### Additional Addresses + ```php /** * Fetch A List Of Contact Additional Addresses @@ -439,6 +613,7 @@ $contactAdditionalAddress = $connector->send(new DeleteAContactAdditionalAddress ``` ### Contact Groups + ```php /** * Fetch A List Of Contact Groups @@ -498,6 +673,7 @@ $contactGroup = $connector->send(new DeleteAContactGroupRequest( ``` ### Contact Relations + ```php /** * Fetch A List Of Contact Relations @@ -561,6 +737,7 @@ $contactRelation = $connector->send(new DeleteAContactRelationRequest( ``` ### Contacts + ```php /** * Fetch A List Of Contacts @@ -657,6 +834,7 @@ $contact = $connector->send(new RestoreAContactRequest( ``` ### Contact Sectors + ```php /** * Fetch A List Of Contact Sectors @@ -676,6 +854,7 @@ $contactSectors = $connector->send(new SearchContactSectorsRequest( ``` ### Currencies + ```php /** * Fetch A List Of Currencies @@ -742,6 +921,7 @@ $exchangeRates = $connector->send(new FetchExchangeRatesForCurrenciesRequest( ``` ### Files + ```php /** * Fetch A List Of Files @@ -823,6 +1003,7 @@ $file = $connector->send(new DeleteAFileRequest( ``` ### Iban Payments + ```php /** * Fetch An Iban Payment @@ -865,7 +1046,7 @@ $payment = $connector->send(new CreateIbanPaymentRequest( ```php /** * Update Iban Payment - * + * * NOTE: THE PAYMENT MUST HAVE A STATUS OF OPEN TO BE UPDATED */ $payment = $connector->send(new EditIbanPaymentRequest( @@ -897,6 +1078,7 @@ $payment = $connector->send(new EditIbanPaymentRequest( ``` ### Invoices + ```php /** * Fetch A List Of Invoices @@ -1058,9 +1240,8 @@ return response(base64_decode($pdf->content)) ->header('Content-Length', $pdf->size); ``` - - ### Languages + ```php /** * Fetch A List Of Languages @@ -1069,6 +1250,7 @@ $languages = $connector->send(new FetchAListOfLanguagesRequest())->dto(); ``` ### Manual Entries + ```php /** * Fetch A List Of Manual Entries @@ -1145,6 +1327,7 @@ $referenceNumber = $connector->send(new GetNextReferenceNumberRequest())->dto(); ``` ### Notes + ```php /** * Fetch A List Of Notes @@ -1208,6 +1391,7 @@ $note = $connector->send(new DeleteANoteRequest( ``` ### Payments + ```php /** * Fetch A List Of Payments @@ -1233,8 +1417,8 @@ $payment = $connector->send(new DeleteAPaymentRequest( ))->json(); ``` - ### Qr Payments + ```php /** * Fetch A Qr Payment @@ -1276,7 +1460,7 @@ $connector->send(new CreateQrPaymentRequest( ```php /** * Update A Qr Payment - * + * * NOTE: THE PAYMENT MUST HAVE A STATUS OF OPEN TO BE UPDATED */ $payment = $connector->send(new EditQrPaymentRequest( @@ -1304,6 +1488,7 @@ $payment = $connector->send(new EditQrPaymentRequest( ``` ### Reports + ```php /** * Journal @@ -1312,6 +1497,7 @@ $journals = $connector->send(new JournalRequest())->dto(); ``` ### Salutations + ```php /** * Fetch A List Of Salutations @@ -1373,6 +1559,7 @@ $salutation = $connector->send(new DeleteASalutationRequest( ``` ### Taxes + ```php /** * Fetch A List Of Taxes @@ -1399,14 +1586,15 @@ $tax = $connector->send(new DeleteATaxRequest( ``` ### Titles + ```php /** * Fetch A List Of Titles */ $titles = $connector->send(new FetchAListOfTitlesRequest())->dto(); - ``` +``` -```php +```php /** * Fetch A Title */ @@ -1460,6 +1648,7 @@ $title = $connector->send(new DeleteATitleRequest( ``` ### VAT Periods + ```php /** * Fetch A List Of VAT Periods @@ -1476,6 +1665,15 @@ $vatPeriod = $connector->send(new FetchAVatPeriodRequest( ))->dto(); ``` +### OpenID Connect + +```php +/** + * Fetch OpenID Userinfo (requires OAuth2) + */ +$userinfo = $connector->send(new FetchUserinfoRequest())->dto(); +``` + #### ## 🚧 Testing @@ -1516,12 +1714,12 @@ Please review [our security policy](.github/SECURITY.md) on reporting security v ## πŸ™ Credits -- [Rhys Lees](https://github.com/RhysLees) -- [Sebastian Fix](https://github.com/StanBarrows) -- [All Contributors](../../contributors) -- [Skeleton Repository from Spatie](https://github.com/spatie/package-skeleton-laravel) -- [Laravel Package Training from Spatie](https://spatie.be/videos/laravel-package-training) -- [Laravel Saloon by Sam CarrΓ©](https://github.com/Sammyjo20/Saloon) +- [Rhys Lees](https://github.com/RhysLees) +- [Sebastian Fix](https://github.com/StanBarrows) +- [All Contributors](../../contributors) +- [Skeleton Repository from Spatie](https://github.com/spatie/package-skeleton-laravel) +- [Laravel Package Training from Spatie](https://spatie.be/videos/laravel-package-training) +- [Laravel Saloon by Sam CarrΓ©](https://github.com/Sammyjo20/Saloon) ## 🎭 License diff --git a/config/bexio.php b/config/bexio.php index 6facf40..0fb136e 100644 --- a/config/bexio.php +++ b/config/bexio.php @@ -2,6 +2,23 @@ return [ 'auth' => [ + 'use_oauth2' => env('BEXIO_USE_OAUTH2', false), 'token' => env('BEXIO_API_TOKEN'), + 'client_id' => env('BEXIO_OAUTH2_CLIENT_ID'), + 'client_secret' => env('BEXIO_OAUTH2_CLIENT_SECRET'), + 'oauth_email' => env('BEXIO_OAUTH2_EMAIL'), + 'scopes' => [], ], + + /* 'auth' => [ + 'token' => env('BEXIO_API_TOKEN'), + + 'oauth2' => [ + 'client_id' => env('BEXIO_OAUTH2_CLIENT_ID'), + 'client_secret' => env('BEXIO_OAUTH2_CLIENT_SECRET'), + 'email' => env('BEXIO_OAUTH2_EMAIL'), + 'scopes' => [], + ],*/ + + 'route_prefix' => 'bexio', ]; diff --git a/resources/views/oauth-result.blade.php b/resources/views/oauth-result.blade.php new file mode 100644 index 0000000..027d6a6 --- /dev/null +++ b/resources/views/oauth-result.blade.php @@ -0,0 +1,28 @@ + + + + + + {{ $title ?? 'Bexio OAuth2 Result' }} + + + + + + +
+

{{ $title }}

+

{{ $message }}

+ @if (isset($actions) && is_array($actions)) +
+ @foreach ($actions as $btn) + {{ $btn['label'] }} + @endforeach +
+ @elseif(isset($action)) + {{ $action['label'] }} + @endif +
+ + + diff --git a/routes/bexio.php b/routes/bexio.php new file mode 100644 index 0000000..04bd992 --- /dev/null +++ b/routes/bexio.php @@ -0,0 +1,15 @@ +prefix(config('bexio.route_prefix', 'bexio'))->group(function () { + Route::get('/oauth/redirect', [BexioOAuthController::class, 'redirect'])->name('bexio.oauth.redirect'); + Route::get('/oauth/callback', [BexioOAuthController::class, 'callback'])->name('bexio.oauth.callback'); +}); diff --git a/src/BexioConnector.php b/src/BexioConnector.php index 836cb36..2222b86 100644 --- a/src/BexioConnector.php +++ b/src/BexioConnector.php @@ -2,15 +2,29 @@ namespace CodebarAg\Bexio; +use CodebarAg\Bexio\Services\BexioOAuthService; +use CodebarAg\Bexio\Support\BexioOAuthTokenStore; use Saloon\Contracts\Authenticator; +use Saloon\Helpers\OAuth2\OAuthConfig; use Saloon\Http\Auth\TokenAuthenticator; use Saloon\Http\Connector; +use Saloon\Http\OAuth2\GetRefreshTokenRequest; +use Saloon\Http\PendingRequest; +use Saloon\Traits\OAuth2\AuthorizationCodeGrant; +use Saloon\Traits\Plugins\AlwaysThrowOnErrors; class BexioConnector extends Connector { + use AlwaysThrowOnErrors, AuthorizationCodeGrant; + public function __construct( protected readonly ?string $token = null, - ) {} + protected ?BexioOAuthTokenStore $tokenStore = null, + protected ?BexioOAuthService $bexioOAuthService = null, + ) { + $this->tokenStore ??= app(BexioOAuthTokenStore::class); + $this->bexioOAuthService ??= app(BexioOAuthService::class); + } public function resolveBaseUrl(): string { @@ -24,8 +38,47 @@ protected function defaultHeaders(): array ]; } + /** + * Saloon boot method: runs before every request. + * Handles token refresh for OAuth2 and sets PAT for legacy tokens. + */ + public function boot(PendingRequest $pendingRequest): void + { + $pendingRequest->middleware()->onRequest(function (PendingRequest $pendingRequest) { + if (config('bexio.auth.use_oauth2')) { + // Prevent recursion: do not refresh while already refreshing + if ($pendingRequest->getRequest() instanceof GetRefreshTokenRequest) { + return; + } + $authenticator = $this->tokenStore->get(); + if ($authenticator && $authenticator->hasExpired()) { + $authenticator = $this->bexioOAuthService->refreshAuthenticator($this->tokenStore, $this); + $pendingRequest->authenticate($authenticator); + } + } + }); + } + protected function defaultAuth(): ?Authenticator { + if (config('bexio.auth.use_oauth2')) { + return $this->tokenStore->get(); + } + return new TokenAuthenticator($this->token ?? config('bexio.auth.token'), 'Bearer'); } + + /** + * Saloon OAuth2 config for Bexio + */ + protected function defaultOauthConfig(): OAuthConfig + { + return OAuthConfig::make() + ->setClientId(config('bexio.auth.client_id')) + ->setClientSecret(config('bexio.auth.client_secret')) + ->setDefaultScopes(['openid', 'offline_access', 'email']) + ->setRedirectUri(route('bexio.oauth.callback')) + ->setAuthorizeEndpoint('https://auth.bexio.com/realms/bexio/protocol/openid-connect/auth') + ->setTokenEndpoint('https://auth.bexio.com/realms/bexio/protocol/openid-connect/token'); + } } diff --git a/src/BexioServiceProvider.php b/src/BexioServiceProvider.php index 5a4d90d..502b5cd 100644 --- a/src/BexioServiceProvider.php +++ b/src/BexioServiceProvider.php @@ -11,13 +11,22 @@ public function configurePackage(Package $package): void { $package ->name('laravel-bexio') - ->hasConfigFile('bexio'); + ->hasConfigFile('bexio') + ->hasRoute('bexio') + ->hasViews('bexio'); } public function bootingPackage() { parent::bootingPackage(); // TODO: Change the autogenerated stub + // Publish controller + if (function_exists('app_path')) { + $this->publishes([ + __DIR__.'/Http/Controllers/BexioOAuthController.php' => app_path('Http/Controllers/BexioOAuthController.php'), + ], 'bexio-controller'); + } + // Issue Temporary Fix: // https://github.com/spatie/laravel-data/pull/699#issuecomment-1995546874 // https://github.com/spatie/laravel-data/issues/731 diff --git a/src/Dto/OpenID/UserinfoDTO.php b/src/Dto/OpenID/UserinfoDTO.php new file mode 100644 index 0000000..3814d36 --- /dev/null +++ b/src/Dto/OpenID/UserinfoDTO.php @@ -0,0 +1,54 @@ +failed()) { + throw new Exception('Failed to create DTO from Response'); + } + $data = $response->json(); + + return self::fromArray($data); + } + + public static function fromArray(array $data): self + { + if (! $data) { + throw new Exception('Unable to create DTO. Data missing from response.'); + } + + return new self( + sub: Arr::get($data, 'sub'), + email: Arr::get($data, 'email'), + email_verified: Arr::get($data, 'email_verified'), + gender: Arr::get($data, 'gender'), + company_id: Arr::get($data, 'company_id'), + company_name: Arr::get($data, 'company_name'), + given_name: Arr::get($data, 'given_name'), + locale: Arr::get($data, 'locale'), + company_user_id: Arr::get($data, 'company_user_id'), + family_name: Arr::get($data, 'family_name'), + ); + } +} diff --git a/src/Exceptions/UserinfoVerificationException.php b/src/Exceptions/UserinfoVerificationException.php new file mode 100644 index 0000000..c711c16 --- /dev/null +++ b/src/Exceptions/UserinfoVerificationException.php @@ -0,0 +1,7 @@ +bexioConnector; + $appScopes = config('bexio.auth.scopes', []); + $authorizationUrl = $connector->getAuthorizationUrl($appScopes); + Session::put('bexio_oauth_state', $connector->getState()); + + return Redirect::away($authorizationUrl); + } catch (\Throwable $e) { + return $this->bexioOAuthExceptionHandler->render($e, 'redirect'); + } + } + + /** + * Handle Bexio OAuth2 callback, exchange code for tokens, and store them. + * + * @return \Illuminate\View\View|\Illuminate\Http\Response + */ + public function callback(Request $request) + { + if ($view = $this->handleBexioCallbackError($request)) { + return $view; + } + + $state = $request->input('state'); + $expectedState = Session::pull('bexio_oauth_state'); + $code = $request->input('code'); + + if (! $code || ! $state || ! $expectedState) { + return $this->bexioOAuthViewBuilder->build( + 'danger', + 'Invalid OAuth Callback', + 'Missing or invalid authorization code/state. Please start the connection process again.', + ['url' => url('/'), 'label' => 'Back to Home'], + null, + 400 + ); + } + + try { + $authenticator = $this->bexioOAuthService->exchangeCodeForAuthenticator($code, $state, $expectedState); + } catch (\Throwable $e) { + return $this->bexioOAuthExceptionHandler->render($e, 'callback'); + } + + try { + $connector = $this->bexioConnector; + $userinfo = $this->bexioOAuthService->fetchUserinfo($authenticator, $connector); + $this->bexioOAuthService->verifyUserinfo($userinfo); + $this->bexioTokenStore->put($authenticator); + + return $this->bexioOAuthViewBuilder->build( + 'success', + 'Bexio Connected!', + 'Your Bexio account was successfully connected.', + ['url' => url('/'), 'label' => 'Back to Home'] + ); + } catch (\Throwable $e) { + return $this->bexioOAuthExceptionHandler->render($e, 'callback'); + } + } + + /** + * Handle errors returned by Bexio during the OAuth callback/redirect process. + * + * This method processes error responses from Bexio when a user is redirected back to the application + * after approving or denying the OAuth authorization request. If the user cancels or Bexio returns an error, + * this method renders an appropriate user-facing error view. + */ + private function handleBexioCallbackError(Request $request): \Illuminate\View\View|\Illuminate\Http\Response|null + { + if ($request->has('error')) { + $error = $request->input('error'); + + if ($error === 'access_denied') { + return $this->bexioOAuthViewBuilder->build( + 'warning', + 'Bexio Connection Cancelled', + 'You cancelled connecting your Bexio account.', + null, + [ + [ + 'url' => url('/'), + 'label' => 'Back to Home', + 'class' => 'secondary', + ], + [ + 'url' => route('bexio.oauth.redirect'), + 'label' => 'Try Again', + 'class' => 'primary', + ], + ], + 200 + ); + } + $description = $request->input('error_description', 'Authorization was denied or failed.'); + $status = $request->input('error') === 'access_denied' ? 400 : 500; + + return $this->bexioOAuthViewBuilder->build( + 'danger', + 'Bexio OAuth2 Error', + $description, + ['url' => url('/'), 'label' => 'Back to Home'], + null, + $status + ); + } + + return null; + } +} diff --git a/src/Requests/OpenID/FetchUserinfoRequest.php b/src/Requests/OpenID/FetchUserinfoRequest.php new file mode 100644 index 0000000..36317c1 --- /dev/null +++ b/src/Requests/OpenID/FetchUserinfoRequest.php @@ -0,0 +1,44 @@ +send(new FetchUserinfoRequest()); + * $userinfo = FetchUserinfoRequest::mapToDTO($response); + */ +class FetchUserinfoRequest extends Request +{ + protected Method $method = Method::GET; + + public function resolveEndpoint(): string + { + return 'https://auth.bexio.com/realms/bexio/protocol/openid-connect/userinfo'; + } + + protected function defaultHeaders(): array + { + return [ + 'Accept' => 'application/json', + ]; + } + + /** + * Instance method for DTO mapping (for consistency with other requests) + */ + public function createDtoFromResponse(Response $response): UserinfoDTO + { + if (! $response->successful()) { + throw new \Exception('Request was not successful. Unable to create DTO.'); + } + + return UserinfoDTO::fromResponse($response); + } +} diff --git a/src/Services/BexioOAuthService.php b/src/Services/BexioOAuthService.php new file mode 100644 index 0000000..24a68ba --- /dev/null +++ b/src/Services/BexioOAuthService.php @@ -0,0 +1,77 @@ +getAccessToken($code, $state, $expectedState); + } + + /** + * Refresh and persist the authenticator, regardless of expiry. + * + * @return AccessTokenAuthenticator|null + */ + public function refreshAuthenticator(BexioOAuthTokenStore $tokenStore, BexioConnector $connector) + { + $authenticator = $tokenStore->get(); + if ($authenticator) { + $authenticator = $connector->refreshAccessToken($authenticator); + $tokenStore->put($authenticator); + } + + return $authenticator; + } + + /** + * Fetch userinfo from Bexio using the authenticator. + * + * @throws \Throwable + */ + public function fetchUserinfo(AccessTokenAuthenticator $authenticator, BexioConnector $connector): array + { + $connector->authenticate($authenticator); + $request = new FetchUserinfoRequest; + $response = $connector->send($request); + $userinfo = $response->json(); + + return $userinfo; + } + + /** + * Verify userinfo claims (email, email_verified). + * + * @throws \CodebarAg\Bexio\Exceptions\UserinfoVerificationException + */ + public function verifyUserinfo(array $userinfo): void + { + $expectedEmail = config('bexio.auth.oauth_email'); + if (! ($userinfo['email_verified'] ?? false) || ($userinfo['email'] ?? null) !== $expectedEmail) { + throw new UserinfoVerificationException( + sprintf( + 'Account verification failed: used email was %s, expected email was %s, email_verified: %s', + $userinfo['email'] ?? 'null', + $expectedEmail ?? 'null', + isset($userinfo['email_verified']) ? var_export($userinfo['email_verified'], true) : 'null' + ) + ); + } + } +} diff --git a/src/Support/BexioOAuthExceptionHandler.php b/src/Support/BexioOAuthExceptionHandler.php new file mode 100644 index 0000000..12aee5a --- /dev/null +++ b/src/Support/BexioOAuthExceptionHandler.php @@ -0,0 +1,104 @@ +error("[BexioOAuth] Exception in $context", ['exception' => $e]); + + if ($e instanceof \Saloon\Exceptions\InvalidStateException) { + return $this->bexioOAuthViewBuilder->build( + 'danger', + 'Invalid OAuth Callback', + 'The authorization state did not match. Please try connecting again.', + ['url' => url('/'), 'label' => 'Back to Home'], + null, + 400 + ); + } + + if ($e instanceof \Saloon\Exceptions\Request\Statuses\UnauthorizedException) { + return $this->bexioOAuthViewBuilder->build( + 'danger', + 'Bexio Authentication Error', + 'Authorization failed. Please ensure your Bexio connection is set up.', + ['url' => url('/'), 'label' => 'Back to Home'], + null, + 401 + ); + } + + if ($e instanceof \Saloon\Exceptions\Request\Statuses\ForbiddenException) { + return $this->bexioOAuthViewBuilder->build( + 'danger', + 'Verification Failed', + 'Verification failed during callback.', + ['url' => url('/'), 'label' => 'Back to Home'], + null, + 403 + ); + } + + if ($e instanceof \Saloon\Exceptions\Request\RequestException) { + return $this->bexioOAuthViewBuilder->build( + 'danger', + 'Bexio API Error', + 'A request to Bexio failed. Please try again later.', + ['url' => url('/'), 'label' => 'Back to Home'], + null, + 500 + ); + } + + if ($e instanceof \Saloon\Exceptions\OAuthConfigValidationException) { + return $this->bexioOAuthViewBuilder->build( + 'danger', + 'OAuth Configuration Error', + 'There was a problem with the OAuth configuration. Please contact support.', + ['url' => url('/'), 'label' => 'Back to Home'], + null, + 500 + ); + } + + if ($e instanceof \CodebarAg\Bexio\Exceptions\UserinfoVerificationException) { + return $this->bexioOAuthViewBuilder->build( + 'danger', + 'Verification Failed', + 'Your account could not be verified. Please contact support or try a different account.', + ['url' => url('/'), 'label' => 'Back to Home'], + null, + 403 + ); + } + + if ($context === 'callback') { + return $this->bexioOAuthViewBuilder->build( + 'danger', + 'Invalid OAuth Callback', + 'Token exchange failed. Please try connecting again.', + ['url' => url('/'), 'label' => 'Back to Home'], + null, + 400 + ); + } + + return $this->bexioOAuthViewBuilder->build( + 'danger', + 'OAuth Error', + 'An unexpected error occurred. Please try again.', + ['url' => url('/'), 'label' => 'Back to Home'], + null, + 500 + ); + } +} diff --git a/src/Support/BexioOAuthTokenStore.php b/src/Support/BexioOAuthTokenStore.php new file mode 100644 index 0000000..73dbabb --- /dev/null +++ b/src/Support/BexioOAuthTokenStore.php @@ -0,0 +1,57 @@ +cacheKey); + if (! $encrypted) { + return null; + } + try { + $serialized = Crypt::decrypt($encrypted); + } catch (\Throwable $e) { + // Could not decrypt, treat as cache miss + return null; + } + + return AccessTokenAuthenticator::unserialize($serialized); + } + + /** + * Store the authenticator in cache (encrypted). + */ + public function put(AccessTokenAuthenticator $authenticator): void + { + $serialized = $authenticator->serialize(); + $encrypted = Crypt::encrypt($serialized); + Cache::put($this->cacheKey, $encrypted); + } + + /** + * Remove the authenticator from cache. + */ + public function forget(): void + { + Cache::forget($this->cacheKey); + } +} diff --git a/src/Support/BexioOAuthViewBuilder.php b/src/Support/BexioOAuthViewBuilder.php new file mode 100644 index 0000000..42c1498 --- /dev/null +++ b/src/Support/BexioOAuthViewBuilder.php @@ -0,0 +1,43 @@ + $type, + 'title' => $title, + 'message' => $message, + ]; + if ($action) { + $data['action'] = $action; + } + if ($actions) { + $data['actions'] = $actions; + } + if ($status) { + return response()->view('bexio::oauth-result', $data, $status); + } + + return view('bexio::oauth-result', $data); + } +} diff --git a/tests/Controllers/BexioOAuthControllerTest.php b/tests/Controllers/BexioOAuthControllerTest.php new file mode 100644 index 0000000..92107e4 --- /dev/null +++ b/tests/Controllers/BexioOAuthControllerTest.php @@ -0,0 +1,120 @@ +set('bexio.auth.client_id', 'fake-client-id'); + config()->set('bexio.auth.client_secret', 'fake-client-secret'); + config()->set('bexio.auth.email', 'test@example.com'); +}); + +it('redirects to Bexio authorization page successfully', function () { + $this->mock(BexioConnector::class, function ($mock) { + $mock->shouldReceive('getAuthorizationUrl')->andReturn('https://bexio.example/authorize'); + $mock->shouldReceive('getState')->andReturn('mocked-state'); + }); + $response = $this->get('/bexio/oauth/redirect'); + $response->assertRedirect('https://bexio.example/authorize'); + $this->assertEquals('mocked-state', session('bexio_oauth_state')); +}); + +it('shows error view on OAuth config error during redirect', function () { + $this->mock(BexioConnector::class, function ($mock) { + $mock->shouldReceive('getAuthorizationUrl')->andThrow(new OAuthConfigValidationException('Config error')); + }); + $response = $this->get('/bexio/oauth/redirect'); + $response->assertStatus(500)->assertSee('OAuth Configuration Error'); +}); + +it('shows error view on API error during redirect', function () { + $this->mock(BexioConnector::class, function ($mock) { + $responseMock = Mockery::mock(Response::class); + $responseMock->shouldReceive('status')->andReturn(500); + $responseMock->shouldReceive('body')->andReturn('API error'); + $mock->shouldReceive('getAuthorizationUrl')->andThrow(new RequestException($responseMock)); + }); + $response = $this->get('/bexio/oauth/redirect'); + $response->assertStatus(500)->assertSee('Bexio API Error'); +}); + +it('shows error view on unauthorized error during redirect', function () { + $this->mock(BexioConnector::class, function ($mock) { + $responseMock = Mockery::mock(Response::class); + $responseMock->shouldReceive('status')->andReturn(401); + $responseMock->shouldReceive('body')->andReturn('Unauthorized error'); + $mock->shouldReceive('getAuthorizationUrl')->andThrow(new UnauthorizedException($responseMock)); + }); + $response = $this->get('/bexio/oauth/redirect'); + $response->assertStatus(401)->assertSee('Bexio Authentication Error'); +}); + +it('shows error view on unexpected error during redirect', function () { + $this->mock(BexioConnector::class, function ($mock) { + $mock->shouldReceive('getAuthorizationUrl')->andThrow(new Exception('Unexpected error')); + }); + $response = $this->get('/bexio/oauth/redirect'); + $response->assertStatus(500)->assertSee('OAuth Error'); +}); + +it('shows cancellation view when user rejects authorization', function () { + $response = $this->get('/bexio/oauth/callback?error=access_denied'); + $response->assertStatus(200) + ->assertSee('Bexio Connection Cancelled') + ->assertSee('You cancelled connecting your Bexio account.'); +}); + +it('handles missing code/state in callback', function () { + $response = $this->get('/bexio/oauth/callback'); + $response->assertStatus(400)->assertSee('Invalid OAuth Callback'); +}); + +it('handles token exchange failure', function () { + $this->mock(BexioConnector::class, function ($mock) { + $mock->shouldReceive('getAccessToken')->andThrow(new Exception('Token error')); + }); + $response = $this->get('/bexio/oauth/callback?code=abc&state=xyz'); + $response->assertStatus(400)->assertSee('Invalid OAuth Callback'); +}); + +it('handles userinfo verification failure', function () { + $this->mock(BexioConnector::class, function ($mock) { + $mock->shouldReceive('getAccessToken')->andReturn('fake-authenticator'); + }); + $this->mock(BexioOAuthService::class, function ($mock) { + $mock->shouldReceive('exchangeCodeForAuthenticator')->andReturn(new AccessTokenAuthenticator('fake-access-token')); + $mock->shouldReceive('fetchUserinfo')->andReturn(['email' => 'fail@example.com']); + $mock->shouldReceive('verifyUserinfo')->andThrow(new UserinfoVerificationException('Verification failed')); + }); + $response = $this + ->withSession(['bexio_oauth_state' => 'failure-state']) + ->get('/bexio/oauth/callback?code=abc&state=failure-state'); + $response->assertStatus(403)->assertSee('Verification Failed'); +}); + +it('stores authenticator and shows success', function () { + $this->mock(BexioConnector::class, function ($mock) { + $mock->shouldReceive('getAccessToken')->andReturn('fake-authenticator'); + }); + $this->mock(BexioOAuthService::class, function ($mock) { + $mock->shouldReceive('exchangeCodeForAuthenticator')->andReturn(new AccessTokenAuthenticator('fake-access-token')); + $mock->shouldReceive('fetchUserinfo')->andReturn(['email' => 'test@example.com']); + $mock->shouldReceive('verifyUserinfo')->andReturnTrue(); + }); + $this->mock(BexioOAuthTokenStore::class, function ($mock) { + $mock->shouldReceive('put')->once(); + }); + $response = $this + ->withSession(['bexio_oauth_state' => 'success-state']) + ->get('/bexio/oauth/callback?code=abc&state=success-state'); + $response->assertStatus(200)->assertSee('Bexio Connected!'); +}); diff --git a/tests/Fixtures/Saloon/OpenID/fetch-userinfo.json b/tests/Fixtures/Saloon/OpenID/fetch-userinfo.json new file mode 100644 index 0000000..19a2e64 --- /dev/null +++ b/tests/Fixtures/Saloon/OpenID/fetch-userinfo.json @@ -0,0 +1,22 @@ +{ + "statusCode": 200, + "headers": { + "Date": "Wed, 11 Jun 2025 00:38:23 GMT", + "Content-Type": "application/json", + "Content-Length": "300", + "Connection": "keep-alive", + "Cache-Control": "no-cache", + "referrer-policy": "no-referrer", + "strict-transport-security": "max-age=31536000; includeSubDomains", + "x-content-type-options": "nosniff", + "x-frame-options": "SAMEORIGIN", + "x-xss-protection": "1; mode=block", + "via": "1.1 google", + "alt-svc": "h3=\":443\"; ma=86400", + "cf-cache-status": "DYNAMIC", + "Server": "cloudflare", + "CF-RAY": "94dd0b9eea10930e-CPH" + }, + "data": "{\"sub\":\"1b1bc12f-5a12-4f86-a631-ac3c4561eaeb\",\"email_verified\":true,\"gender\":\"male\",\"company_id\":\"klmrc5z7edaf\",\"shard_id\":\"42b8db1e-8434-4e78-a8bc-df3b4548fb1f\",\"company_name\":\"Acme Corp\",\"given_name\":\"John\",\"locale\":\"en-GB\",\"company_user_id\":8,\"family_name\":\"Doe\",\"email\":\"john.doe@acme.com\"}", + "context": [] +} diff --git a/tests/Requests/OpenID/FetchUserinfoRequestTest.php b/tests/Requests/OpenID/FetchUserinfoRequestTest.php new file mode 100644 index 0000000..08310a6 --- /dev/null +++ b/tests/Requests/OpenID/FetchUserinfoRequestTest.php @@ -0,0 +1,26 @@ + MockResponse::fixture('OpenID/fetch-userinfo'), + ]); + + $connector = new BexioConnector; + $connector->withMockClient($mockClient); + $response = $connector->send(new FetchUserinfoRequest); + $mockClient->assertSent(FetchUserinfoRequest::class); + + // Assert DTO mapping + $dto = (new FetchUserinfoRequest)->createDtoFromResponse($response); + expect($dto)->toBeInstanceOf(UserinfoDTO::class); + expect($dto->sub)->toBe('1b1bc12f-5a12-4f86-a631-ac3c4561eaeb'); + expect($dto->email)->toBe('john.doe@acme.com'); + expect($dto->company_name)->toBe('Acme Corp'); + expect($dto->email_verified)->toBeTrue(); +}); diff --git a/tests/Services/BexioOAuthServiceTest.php b/tests/Services/BexioOAuthServiceTest.php new file mode 100644 index 0000000..9fd10ac --- /dev/null +++ b/tests/Services/BexioOAuthServiceTest.php @@ -0,0 +1,105 @@ + MockResponse::make([ + 'email' => 'test@example.com', + 'email_verified' => true, + ], 200), + ]); + $connector = new BexioConnector; + $connector->withMockClient($mockClient); + $service = new BexioOAuthService; + $authenticator = Mockery::mock(\Saloon\Http\Auth\AccessTokenAuthenticator::class); + $authenticator->shouldReceive('set')->andReturnNull(); + $userinfo = $service->fetchUserinfo($authenticator, $connector); + expect($userinfo['email'])->toBe('test@example.com'); + expect($userinfo['email_verified'])->toBeTrue(); + $mockClient->assertSent(FetchUserinfoRequest::class); +}); + +describe('BexioOAuthService', function () { + it('can exchange code for authenticator', function () { + $mockConnector = Mockery::mock(CodebarAg\Bexio\BexioConnector::class); + $mockAuthenticator = Mockery::mock(\Saloon\Contracts\OAuthAuthenticator::class); + $mockConnector->shouldReceive('getAccessToken') + ->with('valid-code', 'state', 'expected-state') + ->andReturn($mockAuthenticator); + $service = Mockery::mock(BexioOAuthService::class)->makePartial(); + $service->shouldReceive('exchangeCodeForAuthenticator') + ->with('valid-code', 'state', 'expected-state') + ->andReturnUsing(function ($code, $state, $expectedState) use ($mockConnector) { + return $mockConnector->getAccessToken($code, $state, $expectedState); + }); + $authenticator = $service->exchangeCodeForAuthenticator('valid-code', 'state', 'expected-state'); + expect($authenticator)->toBe($mockAuthenticator); + }); + + it('throws on token exchange failure', function () { + $mockConnector = Mockery::mock(CodebarAg\Bexio\BexioConnector::class); + $mockConnector->shouldReceive('getAccessToken') + ->with('bad-code', 'state', 'expected-state') + ->andThrow(new Exception('Token error')); + $service = Mockery::mock(BexioOAuthService::class)->makePartial(); + $service->shouldReceive('exchangeCodeForAuthenticator') + ->with('bad-code', 'state', 'expected-state') + ->andReturnUsing(function ($code, $state, $expectedState) use ($mockConnector) { + return $mockConnector->getAccessToken($code, $state, $expectedState); + }); + expect(fn () => $service->exchangeCodeForAuthenticator('bad-code', 'state', 'expected-state')) + ->toThrow(Exception::class); + }); + + it('refreshes and persists authenticator', function () { + $store = Mockery::mock(CodebarAg\Bexio\Support\BexioOAuthTokenStore::class); + $connector = Mockery::mock(CodebarAg\Bexio\BexioConnector::class); + $oldAuth = Mockery::mock(\Saloon\Http\Auth\AccessTokenAuthenticator::class); + $newAuth = Mockery::mock(\Saloon\Http\Auth\AccessTokenAuthenticator::class); + $store->shouldReceive('get')->andReturn($oldAuth); + $connector->shouldReceive('refreshAccessToken')->with($oldAuth)->andReturn($newAuth); + $store->shouldReceive('put')->with($newAuth); + $oldAuth->shouldReceive('getAccessToken')->andReturn('old-token'); + $newAuth->shouldReceive('getAccessToken')->andReturn('new-token'); + $newAuth->shouldReceive('getExpiresAt')->andReturn(null); + $service = new BexioOAuthService; + $result = $service->refreshAuthenticator($store, $connector); + expect($result)->toBe($newAuth); + }); + + it('returns null if no authenticator to refresh', function () { + $store = Mockery::mock(CodebarAg\Bexio\Support\BexioOAuthTokenStore::class); + $connector = Mockery::mock(CodebarAg\Bexio\BexioConnector::class); + $store->shouldReceive('get')->andReturn(null); + $service = new BexioOAuthService; + $result = $service->refreshAuthenticator($store, $connector); + expect($result)->toBeNull(); + }); + + it('verifyUserinfo passes with correct data', function () { + $service = new BexioOAuthService; + config(['bexio.auth.oauth_email' => 'test@example.com']); + $userinfo = ['email' => 'test@example.com', 'email_verified' => true]; + $service->verifyUserinfo($userinfo); + expect(true)->toBeTrue(); + }); + + it('verifyUserinfo throws on unverified email', function () { + $service = new BexioOAuthService; + config(['bexio.auth.oauth_email' => 'test@example.com']); + $userinfo = ['email' => 'test@example.com', 'email_verified' => false]; + expect(fn () => $service->verifyUserinfo($userinfo))->toThrow(Exception::class); + }); + + it('verifyUserinfo throws on wrong email', function () { + $service = new BexioOAuthService; + config(['bexio.auth.oauth_email' => 'test@example.com']); + $userinfo = ['email' => 'wrong@example.com', 'email_verified' => true]; + expect(fn () => $service->verifyUserinfo($userinfo))->toThrow(Exception::class); + }); +}); diff --git a/tests/Support/BexioTokenStoreTest.php b/tests/Support/BexioTokenStoreTest.php new file mode 100644 index 0000000..bffc3e0 --- /dev/null +++ b/tests/Support/BexioTokenStoreTest.php @@ -0,0 +1,39 @@ +get())->toBeNull(); +}); + +it('overwrites the authenticator', function () { + $auth1 = new AccessTokenAuthenticator('token1', 'Bearer'); + $auth2 = new AccessTokenAuthenticator('token2', 'Bearer'); + $store = new BexioOAuthTokenStore; + $store->put($auth1); + expect($store->get())->toEqual($auth1); + $store->put($auth2); + expect($store->get())->toEqual($auth2); +}); + +it('stores and retrieves authenticator', function () { + $auth = new AccessTokenAuthenticator('token', 'Bearer'); + $store = new BexioOAuthTokenStore; + $store->put($auth); + expect($store->get())->toEqual($auth); +}); + +it('forgets the authenticator', function () { + $auth = new AccessTokenAuthenticator('token', 'Bearer'); + $store = new BexioOAuthTokenStore; + $store->put($auth); + $store->forget(); + expect($store->get())->toBeNull(); +});