Compare commits

..

2 Commits

Author SHA1 Message Date
HiteshRepo
3d1fa661bf move M365 creds rotation logic to code 2024-02-05 20:57:42 +05:30
HiteshRepo
6d2027d6c2 enables cache for diff types of tests 2024-02-05 13:25:44 +05:30
181 changed files with 2803 additions and 11460 deletions

View File

@ -1,5 +1,4 @@
name: Backup Restore Test name: Backup Restore Test
description: Run various backup/restore/export tests for a service.
inputs: inputs:
service: service:

View File

@ -1,5 +1,4 @@
name: Setup and Cache Golang name: Setup and Cache Golang
description: Build golang binaries for later use in CI.
# clone of: https://github.com/magnetikonline/action-golang-cache/blob/main/action.yaml # clone of: https://github.com/magnetikonline/action-golang-cache/blob/main/action.yaml
# #

View File

@ -1,5 +1,4 @@
name: Publish Binary name: Publish Binary
description: Publish binary artifacts.
inputs: inputs:
version: version:

View File

@ -1,5 +1,4 @@
name: Publish Website name: Publish Website
description: Publish website artifacts.
inputs: inputs:
aws-iam-role: aws-iam-role:

View File

@ -1,5 +1,4 @@
name: Purge M365 User Data name: Purge M365 User Data
description: Deletes M365 data generated during CI tests.
# Hard deletion of an m365 user's data. Our CI processes create a lot # Hard deletion of an m365 user's data. Our CI processes create a lot
# of data churn (creation and immediate deletion) of files, the likes # of data churn (creation and immediate deletion) of files, the likes
@ -31,19 +30,12 @@ inputs:
description: Secret value of for AZURE_CLIENT_ID description: Secret value of for AZURE_CLIENT_ID
azure-client-secret: azure-client-secret:
description: Secret value of for AZURE_CLIENT_SECRET description: Secret value of for AZURE_CLIENT_SECRET
azure-pnp-client-id:
description: Secret value of AZURE_PNP_CLIENT_ID
azure-pnp-client-cert:
description: Base64 encoded private certificate for the azure-pnp-client-id (Secret value of AZURE_PNP_CLIENT_CERT)
azure-tenant-id: azure-tenant-id:
description: Secret value of AZURE_TENANT_ID description: Secret value of for AZURE_TENANT_ID
m365-admin-user: m365-admin-user:
description: Secret value of for M365_TENANT_ADMIN_USER description: Secret value of for M365_TENANT_ADMIN_USER
m365-admin-password: m365-admin-password:
description: Secret value of for M365_TENANT_ADMIN_PASSWORD description: Secret value of for M365_TENANT_ADMIN_PASSWORD
tenant-domain:
description: The domain of the tenant (ex. 10rqc2.onmicrosft.com)
required: true
runs: runs:
using: composite using: composite
@ -61,13 +53,7 @@ runs:
AZURE_CLIENT_ID: ${{ inputs.azure-client-id }} AZURE_CLIENT_ID: ${{ inputs.azure-client-id }}
AZURE_CLIENT_SECRET: ${{ inputs.azure-client-secret }} AZURE_CLIENT_SECRET: ${{ inputs.azure-client-secret }}
AZURE_TENANT_ID: ${{ inputs.azure-tenant-id }} AZURE_TENANT_ID: ${{ inputs.azure-tenant-id }}
run: | run: ./exchangePurge.ps1 -User ${{ inputs.user }} -FolderNamePurgeList PersonMetadata -FolderPrefixPurgeList "${{ inputs.folder-prefix }}".Split(",") -PurgeBeforeTimestamp ${{ inputs.older-than }}
for ($ATTEMPT_NUM = 1; $ATTEMPT_NUM -le 3; $ATTEMPT_NUM++)
{
if (./exchangePurge.ps1 -User ${{ inputs.user }} -FolderNamePurgeList PersonMetadata -FolderPrefixPurgeList "${{ inputs.folder-prefix }}".Split(",") -PurgeBeforeTimestamp ${{ inputs.older-than }}) {
break
}
}
# TODO(ashmrtn): Re-enable when we figure out errors we're seeing with Get-Mailbox call. # TODO(ashmrtn): Re-enable when we figure out errors we're seeing with Get-Mailbox call.
#- name: Reset retention for all mailboxes to 0 #- name: Reset retention for all mailboxes to 0
@ -88,16 +74,10 @@ runs:
shell: pwsh shell: pwsh
working-directory: ./src/cmd/purge/scripts working-directory: ./src/cmd/purge/scripts
env: env:
AZURE_CLIENT_ID: ${{ inputs.azure-pnp-client-id }} M365_TENANT_ADMIN_USER: ${{ inputs.m365-admin-user }}
AZURE_APP_CERT: ${{ inputs.azure-pnp-client-cert }} M365_TENANT_ADMIN_PASSWORD: ${{ inputs.m365-admin-password }}
TENANT_DOMAIN: ${{ inputs.tenant-domain }}
run: | run: |
for ($ATTEMPT_NUM = 1; $ATTEMPT_NUM -le 3; $ATTEMPT_NUM++) ./onedrivePurge.ps1 -User ${{ inputs.user }} -FolderPrefixPurgeList "${{ inputs.folder-prefix }}".Split(",") -PurgeBeforeTimestamp ${{ inputs.older-than }}
{
if (./onedrivePurge.ps1 -User ${{ inputs.user }} -FolderPrefixPurgeList "${{ inputs.folder-prefix }}".Split(",") -PurgeBeforeTimestamp ${{ inputs.older-than }}) {
break
}
}
################################################################################################################ ################################################################################################################
# Sharepoint # Sharepoint
@ -108,14 +88,6 @@ runs:
shell: pwsh shell: pwsh
working-directory: ./src/cmd/purge/scripts working-directory: ./src/cmd/purge/scripts
env: env:
AZURE_CLIENT_ID: ${{ inputs.azure-pnp-client-id }} M365_TENANT_ADMIN_USER: ${{ inputs.m365-admin-user }}
AZURE_APP_CERT: ${{ inputs.azure-pnp-client-cert }} M365_TENANT_ADMIN_PASSWORD: ${{ inputs.m365-admin-password }}
TENANT_DOMAIN: ${{ inputs.tenant-domain }} run: ./onedrivePurge.ps1 -Site ${{ inputs.site }} -LibraryNameList "${{ inputs.libraries }}".split(",") -FolderPrefixPurgeList ${{ inputs.folder-prefix }} -LibraryPrefixDeleteList ${{ inputs.library-prefix && inputs.library-prefix || '[]' }} -PurgeBeforeTimestamp ${{ inputs.older-than }}
run: |
for ($ATTEMPT_NUM = 1; $ATTEMPT_NUM -le 3; $ATTEMPT_NUM++)
{
if (./onedrivePurge.ps1 -Site ${{ inputs.site }} -LibraryNameList "${{ inputs.libraries }}".split(",") -FolderPrefixPurgeList ${{ inputs.folder-prefix }} -LibraryPrefixDeleteList ${{ inputs.library-prefix && inputs.library-prefix || '[]' }} -PurgeBeforeTimestamp ${{ inputs.older-than }}) {
break
}
}

View File

@ -1,5 +1,4 @@
name: Send a message to Teams name: Send a message to Teams
description: Send messages to communication apps.
inputs: inputs:
msg: msg:

View File

@ -1,5 +1,4 @@
name: Lint Website name: Lint Website
description: Lint website content.
inputs: inputs:
version: version:

View File

@ -40,5 +40,5 @@ jobs:
if: failure() if: failure()
uses: ./.github/actions/teams-message uses: ./.github/actions/teams-message
with: with:
msg: "[CORSO FAILED] Publishing Binary" msg: "[FAILED] Publishing Binary"
teams_url: ${{ secrets.TEAMS_CORSO_CI_WEBHOOK_URL }} teams_url: ${{ secrets.TEAMS_CORSO_CI_WEBHOOK_URL }}

View File

@ -139,17 +139,28 @@ jobs:
AZURE_CLIENT_ID_NAME: ${{ needs.SetM365App.outputs.client_id_env }} AZURE_CLIENT_ID_NAME: ${{ needs.SetM365App.outputs.client_id_env }}
AZURE_CLIENT_SECRET_NAME: ${{ needs.SetM365App.outputs.client_secret_env }} AZURE_CLIENT_SECRET_NAME: ${{ needs.SetM365App.outputs.client_secret_env }}
CLIENT_APP_SLOT: ${{ needs.SetM365App.outputs.client_app_slot }} CLIENT_APP_SLOT: ${{ needs.SetM365App.outputs.client_app_slot }}
CORSO_LOG_FILE: ${{ github.workspace }}/src/testlog/run-ci.log CORSO_LOG_FILE: /tmp/corso-trusted-testlog/run-ci.log
LOG_GRAPH_REQUESTS: true LOG_GRAPH_REQUESTS: true
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Golang with cache - name: Setup Golang with cache
uses: magnetikonline/action-golang-cache@v4 uses: actions/setup-go@v5
with: with:
go-version-file: src/go.mod go-version: 1.21
cache: true
- run: mkdir testlog - name: Setup Golang caches
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-golang-trusted-${{ github.sha }}
restore-keys: |
${{ runner.os }}-golang-trusted-
- run: mkdir -p /tmp/corso-trusted-testlog
# Install gotestfmt # Install gotestfmt
- name: Set up gotestfmt - name: Set up gotestfmt
@ -166,8 +177,8 @@ jobs:
# run the tests # run the tests
- name: Integration Tests - name: Integration Tests
env: env:
AZURE_CLIENT_ID: ${{ secrets[env.AZURE_CLIENT_ID_NAME] }} AZURE_CLIENT_ID: ${{ secrets[CLIENT_ID] }},${{ secrets[CLIENT_ID_2] }},${{ secrets[CLIENT_ID_3] }},${{ secrets[CLIENT_ID_4] }}
AZURE_CLIENT_SECRET: ${{ secrets[env.AZURE_CLIENT_SECRET_NAME] }} AZURE_CLIENT_SECRET: ${{ secrets[CLIENT_SECRET] }},${{ secrets[CLIENT_SECRET_2] }},${{ secrets[CLIENT_SECRET_3] }},${{ secrets[CLIENT_SECRET_4] }}
AZURE_TENANT_ID: ${{ secrets.TENANT_ID }} AZURE_TENANT_ID: ${{ secrets.TENANT_ID }}
CORSO_CI_TESTS: true CORSO_CI_TESTS: true
CORSO_M365_TEST_USER_ID: ${{ vars.CORSO_M365_TEST_USER_ID }} CORSO_M365_TEST_USER_ID: ${{ vars.CORSO_M365_TEST_USER_ID }}
@ -184,15 +195,15 @@ jobs:
-p 1 \ -p 1 \
-timeout 20m \ -timeout 20m \
./... \ ./... \
2>&1 | tee ./testlog/gotest-ci.log | gotestfmt -hide successful-tests 2>&1 | tee /tmp/corso-trusted-testlog/gotest-ci.log | gotestfmt -hide successful-tests
# Upload the original go test output as an artifact for later review. # Upload the original go test output as an artifact for later review.
- name: Upload test log - name: Upload test log
if: failure() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ci-test-log name: ci-test-log
path: src/testlog/* path: /tmp/corso-trusted-testlog/*
if-no-files-found: error if-no-files-found: error
retention-days: 14 retention-days: 14
@ -210,17 +221,28 @@ jobs:
AZURE_CLIENT_ID_NAME: ${{ needs.SetM365App.outputs.client_id_env }} AZURE_CLIENT_ID_NAME: ${{ needs.SetM365App.outputs.client_id_env }}
AZURE_CLIENT_SECRET_NAME: ${{ needs.SetM365App.outputs.client_secret_env }} AZURE_CLIENT_SECRET_NAME: ${{ needs.SetM365App.outputs.client_secret_env }}
CLIENT_APP_SLOT: ${{ needs.SetM365App.outputs.client_app_slot }} CLIENT_APP_SLOT: ${{ needs.SetM365App.outputs.client_app_slot }}
CORSO_LOG_FILE: ${{ github.workspace }}/src/testlog/run-ci-retention.log CORSO_LOG_FILE: /tmp/corso-retention-testlog/run-ci-retention.log
LOG_GRAPH_REQUESTS: true LOG_GRAPH_REQUESTS: true
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Golang with cache - name: Setup Golang with cache
uses: magnetikonline/action-golang-cache@v4 uses: actions/setup-go@v5
with: with:
go-version-file: src/go.mod go-version: 1.21
cache: true
- run: mkdir testlog - name: Setup Golang caches
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-golang-retention-${{ github.sha }}
restore-keys: |
${{ runner.os }}-golang-retention-
- run: mkdir -p /tmp/corso-retention-testlog
# Install gotestfmt # Install gotestfmt
- name: Set up gotestfmt - name: Set up gotestfmt
@ -237,8 +259,8 @@ jobs:
# run the tests # run the tests
- name: Retention Tests - name: Retention Tests
env: env:
AZURE_CLIENT_ID: ${{ secrets[env.AZURE_CLIENT_ID_NAME] }} AZURE_CLIENT_ID: ${{ secrets[CLIENT_ID] }},${{ secrets[CLIENT_ID_2] }},${{ secrets[CLIENT_ID_3] }},${{ secrets[CLIENT_ID_4] }}
AZURE_CLIENT_SECRET: ${{ secrets[env.AZURE_CLIENT_SECRET_NAME] }} AZURE_CLIENT_SECRET: ${{ secrets[CLIENT_SECRET] }},${{ secrets[CLIENT_SECRET_2] }},${{ secrets[CLIENT_SECRET_3] }},${{ secrets[CLIENT_SECRET_4] }}
AZURE_TENANT_ID: ${{ secrets.TENANT_ID }} AZURE_TENANT_ID: ${{ secrets.TENANT_ID }}
CORSO_RETENTION_TESTS: true CORSO_RETENTION_TESTS: true
CORSO_M365_TEST_USER_ID: ${{ vars.CORSO_M365_TEST_USER_ID }} CORSO_M365_TEST_USER_ID: ${{ vars.CORSO_M365_TEST_USER_ID }}
@ -255,15 +277,15 @@ jobs:
-p 1 \ -p 1 \
-timeout 10m \ -timeout 10m \
./... \ ./... \
2>&1 | tee ./testlog/gotest-ci.log | gotestfmt -hide successful-tests 2>&1 | tee /tmp/corso-retention-testlog/gotest-ci.log | gotestfmt -hide successful-tests
# Upload the original go test output as an artifact for later review. # Upload the original go test output as an artifact for later review.
- name: Upload test log - name: Upload test log
if: failure() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ci-retention-test-log name: ci-retention-test-log
path: src/testlog/* path: /tmp/corso-trusted-testlog/*
if-no-files-found: error if-no-files-found: error
retention-days: 14 retention-days: 14
@ -277,17 +299,28 @@ jobs:
run: run:
working-directory: src working-directory: src
env: env:
CORSO_LOG_FILE: ${{ github.workspace }}/src/testlog/run-unit.log CORSO_LOG_FILE: /tmp/corso-unit-testlog/run-unit.log
LOG_GRAPH_REQUESTS: true LOG_GRAPH_REQUESTS: true
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Golang with cache - name: Setup Golang with cache
uses: magnetikonline/action-golang-cache@v4 uses: actions/setup-go@v5
with: with:
go-version-file: src/go.mod go-version: 1.21
cache: true
- run: mkdir testlog - name: Setup Golang caches
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-golang-unit-${{ github.sha }}
restore-keys: |
${{ runner.os }}-golang-unit-
- run: mkdir -p /tmp/corso-unit-testlog
# Install gotestfmt # Install gotestfmt
- name: Set up gotestfmt - name: Set up gotestfmt
@ -310,15 +343,15 @@ jobs:
-p 1 \ -p 1 \
-timeout 20m \ -timeout 20m \
./... \ ./... \
2>&1 | tee ./testlog/gotest-unit.log | gotestfmt -hide successful-tests 2>&1 | tee /tmp/corso-unit-testlog/gotest-unit.log | gotestfmt -hide successful-tests
# Upload the original go test output as an artifact for later review. # Upload the original go test output as an artifact for later review.
- name: Upload test log - name: Upload test log
if: failure() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: unit-test-log name: unit-test-log
path: src/testlog/* path: /tmp/corso-unit-testlog/*
if-no-files-found: error if-no-files-found: error
retention-days: 14 retention-days: 14
@ -463,7 +496,7 @@ jobs:
go-version-file: src/go.mod go-version-file: src/go.mod
- name: Go Lint - name: Go Lint
uses: golangci/golangci-lint-action@v4 uses: golangci/golangci-lint-action@v3
with: with:
# Keep pinned to a verson as sometimes updates will add new lint # Keep pinned to a verson as sometimes updates will add new lint
# failures in unchanged code. # failures in unchanged code.

View File

@ -12,7 +12,7 @@ jobs:
continue-on-error: true continue-on-error: true
strategy: strategy:
matrix: matrix:
user: [CORSO_M365_TEST_USER_ID, CORSO_SECONDARY_M365_TEST_USER_ID, ""] user: [ CORSO_M365_TEST_USER_ID, CORSO_SECONDARY_M365_TEST_USER_ID, '' ]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -33,15 +33,12 @@ jobs:
azure-tenant-id: ${{ secrets.TENANT_ID }} azure-tenant-id: ${{ secrets.TENANT_ID }}
m365-admin-user: ${{ secrets.M365_TENANT_ADMIN_USER }} m365-admin-user: ${{ secrets.M365_TENANT_ADMIN_USER }}
m365-admin-password: ${{ secrets.M365_TENANT_ADMIN_PASSWORD }} m365-admin-password: ${{ secrets.M365_TENANT_ADMIN_PASSWORD }}
azure-pnp-client-id: ${{ secrets.AZURE_PNP_CLIENT_ID }}
azure-pnp-client-cert: ${{ secrets.AZURE_PNP_CLIENT_CERT }}
tenant-domain: ${{ vars.TENANT_DOMAIN }}
- name: Notify failure in teams - name: Notify failure in teams
if: failure() if: failure()
uses: ./.github/actions/teams-message uses: ./.github/actions/teams-message
with: with:
msg: "[CORSO FAILED] ${{ vars[matrix.user] }} CI Cleanup" msg: "[FAILED] ${{ vars[matrix.user] }} CI Cleanup"
teams_url: ${{ secrets.TEAMS_CORSO_CI_WEBHOOK_URL }} teams_url: ${{ secrets.TEAMS_CORSO_CI_WEBHOOK_URL }}
Test-Site-Data-Cleanup: Test-Site-Data-Cleanup:
@ -73,13 +70,10 @@ jobs:
azure-tenant-id: ${{ secrets.TENANT_ID }} azure-tenant-id: ${{ secrets.TENANT_ID }}
m365-admin-user: ${{ secrets.M365_TENANT_ADMIN_USER }} m365-admin-user: ${{ secrets.M365_TENANT_ADMIN_USER }}
m365-admin-password: ${{ secrets.M365_TENANT_ADMIN_PASSWORD }} m365-admin-password: ${{ secrets.M365_TENANT_ADMIN_PASSWORD }}
azure-pnp-client-id: ${{ secrets.AZURE_PNP_CLIENT_ID }}
azure-pnp-client-cert: ${{ secrets.AZURE_PNP_CLIENT_CERT }}
tenant-domain: ${{ vars.TENANT_DOMAIN }}
- name: Notify failure in teams - name: Notify failure in teams
if: failure() if: failure()
uses: ./.github/actions/teams-message uses: ./.github/actions/teams-message
with: with:
msg: "[CORSO FAILED] ${{ vars[matrix.site] }} CI Cleanup" msg: "[FAILED] ${{ vars[matrix.site] }} CI Cleanup"
teams_url: ${{ secrets.TEAMS_CORSO_CI_WEBHOOK_URL }} teams_url: ${{ secrets.TEAMS_CORSO_CI_WEBHOOK_URL }}

View File

@ -155,6 +155,3 @@ jobs:
azure-tenant-id: ${{ secrets.TENANT_ID }} azure-tenant-id: ${{ secrets.TENANT_ID }}
m365-admin-user: ${{ secrets.M365_TENANT_ADMIN_USER }} m365-admin-user: ${{ secrets.M365_TENANT_ADMIN_USER }}
m365-admin-password: ${{ secrets.M365_TENANT_ADMIN_PASSWORD }} m365-admin-password: ${{ secrets.M365_TENANT_ADMIN_PASSWORD }}
azure-pnp-client-id: ${{ secrets.AZURE_PNP_CLIENT_ID }}
azure-pnp-client-cert: ${{ secrets.AZURE_PNP_CLIENT_CERT }}
tenant-domain: ${{ vars.TENANT_DOMAIN }}

View File

@ -6,7 +6,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
user: user:
description: "User to run longevity test on" description: 'User to run longevity test on'
permissions: permissions:
# required to retrieve AWS credentials # required to retrieve AWS credentials
@ -37,7 +37,7 @@ jobs:
CORSO_LOG_FILE: ${{ github.workspace }}/src/testlog/run-longevity.log CORSO_LOG_FILE: ${{ github.workspace }}/src/testlog/run-longevity.log
RESTORE_DEST_PFX: Corso_Test_Longevity_ RESTORE_DEST_PFX: Corso_Test_Longevity_
TEST_USER: ${{ github.event.inputs.user != '' && github.event.inputs.user || vars.CORSO_M365_TEST_USER_ID }} TEST_USER: ${{ github.event.inputs.user != '' && github.event.inputs.user || vars.CORSO_M365_TEST_USER_ID }}
PREFIX: "longevity" PREFIX: 'longevity'
# Options for retention. # Options for retention.
RETENTION_MODE: GOVERNANCE RETENTION_MODE: GOVERNANCE
@ -113,6 +113,7 @@ jobs:
--extend-retention \ --extend-retention \
--prefix ${{ env.PREFIX }} \ --prefix ${{ env.PREFIX }} \
--bucket ${{ secrets.CI_RETENTION_TESTS_S3_BUCKET }} \ --bucket ${{ secrets.CI_RETENTION_TESTS_S3_BUCKET }} \
--succeed-if-exists \
2>&1 | tee ${{ env.CORSO_LOG_DIR }}/gotest-repo-init.log 2>&1 | tee ${{ env.CORSO_LOG_DIR }}/gotest-repo-init.log
if grep -q 'Failed to' ${{ env.CORSO_LOG_DIR }}/gotest-repo-init.log if grep -q 'Failed to' ${{ env.CORSO_LOG_DIR }}/gotest-repo-init.log
@ -392,5 +393,5 @@ jobs:
if: failure() if: failure()
uses: ./.github/actions/teams-message uses: ./.github/actions/teams-message
with: with:
msg: "[CORSO FAILED] Longevity Test" msg: "[FAILED] Longevity Test"
teams_url: ${{ secrets.TEAMS_CORSO_CI_WEBHOOK_URL }} teams_url: ${{ secrets.TEAMS_CORSO_CI_WEBHOOK_URL }}

View File

@ -118,5 +118,5 @@ jobs:
if: failure() if: failure()
uses: ./.github/actions/teams-message uses: ./.github/actions/teams-message
with: with:
msg: "[COROS FAILED] Nightly Checks" msg: "[FAILED] Nightly Checks"
teams_url: ${{ secrets.TEAMS_CORSO_CI_WEBHOOK_URL }} teams_url: ${{ secrets.TEAMS_CORSO_CI_WEBHOOK_URL }}

View File

@ -19,7 +19,7 @@ jobs:
private_key: ${{ secrets.PRIVATE_KEY }} private_key: ${{ secrets.PRIVATE_KEY }}
- name: Slash Command Dispatch - name: Slash Command Dispatch
uses: peter-evans/slash-command-dispatch@v4 uses: peter-evans/slash-command-dispatch@v3
env: env:
TOKEN: ${{ steps.generate_token.outputs.token }} TOKEN: ${{ steps.generate_token.outputs.token }}
with: with:

View File

@ -6,7 +6,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
user: user:
description: "User to run sanity test on" description: 'User to run sanity test on'
permissions: permissions:
# required to retrieve AWS credentials # required to retrieve AWS credentials
@ -48,6 +48,7 @@ jobs:
# setup # setup
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Golang with cache - name: Setup Golang with cache
@ -90,9 +91,6 @@ jobs:
azure-tenant-id: ${{ secrets.TENANT_ID }} azure-tenant-id: ${{ secrets.TENANT_ID }}
m365-admin-user: ${{ secrets.M365_TENANT_ADMIN_USER }} m365-admin-user: ${{ secrets.M365_TENANT_ADMIN_USER }}
m365-admin-password: ${{ secrets.M365_TENANT_ADMIN_PASSWORD }} m365-admin-password: ${{ secrets.M365_TENANT_ADMIN_PASSWORD }}
azure-pnp-client-id: ${{ secrets.AZURE_PNP_CLIENT_ID }}
azure-pnp-client-cert: ${{ secrets.AZURE_PNP_CLIENT_CERT }}
tenant-domain: ${{ vars.TENANT_DOMAIN }}
- name: Purge CI-Produced Folders for Sites - name: Purge CI-Produced Folders for Sites
timeout-minutes: 30 timeout-minutes: 30
@ -108,9 +106,6 @@ jobs:
azure-tenant-id: ${{ secrets.TENANT_ID }} azure-tenant-id: ${{ secrets.TENANT_ID }}
m365-admin-user: ${{ secrets.M365_TENANT_ADMIN_USER }} m365-admin-user: ${{ secrets.M365_TENANT_ADMIN_USER }}
m365-admin-password: ${{ secrets.M365_TENANT_ADMIN_PASSWORD }} m365-admin-password: ${{ secrets.M365_TENANT_ADMIN_PASSWORD }}
azure-pnp-client-id: ${{ secrets.AZURE_PNP_CLIENT_ID }}
azure-pnp-client-cert: ${{ secrets.AZURE_PNP_CLIENT_CERT }}
tenant-domain: ${{ vars.TENANT_DOMAIN }}
########################################################################################################################################## ##########################################################################################################################################
@ -198,8 +193,8 @@ jobs:
service: exchange service: exchange
kind: first-backup kind: first-backup
backup-args: '--mailbox "${{ env.TEST_USER }}" --data "email"' backup-args: '--mailbox "${{ env.TEST_USER }}" --data "email"'
restore-args: "--email-folder ${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}" restore-args: '--email-folder ${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
restore-container: "${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}" restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
log-dir: ${{ env.CORSO_LOG_DIR }} log-dir: ${{ env.CORSO_LOG_DIR }}
with-export: true with-export: true
@ -211,8 +206,8 @@ jobs:
service: exchange service: exchange
kind: incremental kind: incremental
backup-args: '--mailbox "${{ env.TEST_USER }}" --data "email"' backup-args: '--mailbox "${{ env.TEST_USER }}" --data "email"'
restore-args: "--email-folder ${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}" restore-args: '--email-folder ${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
restore-container: "${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}" restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
backup-id: ${{ steps.exchange-backup.outputs.backup-id }} backup-id: ${{ steps.exchange-backup.outputs.backup-id }}
log-dir: ${{ env.CORSO_LOG_DIR }} log-dir: ${{ env.CORSO_LOG_DIR }}
with-export: true with-export: true
@ -225,8 +220,8 @@ jobs:
service: exchange service: exchange
kind: non-delta kind: non-delta
backup-args: '--mailbox "${{ env.TEST_USER }}" --data "email" --disable-delta' backup-args: '--mailbox "${{ env.TEST_USER }}" --data "email" --disable-delta'
restore-args: "--email-folder ${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}" restore-args: '--email-folder ${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
restore-container: "${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}" restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
backup-id: ${{ steps.exchange-backup.outputs.backup-id }} backup-id: ${{ steps.exchange-backup.outputs.backup-id }}
log-dir: ${{ env.CORSO_LOG_DIR }} log-dir: ${{ env.CORSO_LOG_DIR }}
with-export: true with-export: true
@ -239,12 +234,13 @@ jobs:
service: exchange service: exchange
kind: non-delta-incremental kind: non-delta-incremental
backup-args: '--mailbox "${{ env.TEST_USER }}" --data "email"' backup-args: '--mailbox "${{ env.TEST_USER }}" --data "email"'
restore-args: "--email-folder ${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}" restore-args: '--email-folder ${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
restore-container: "${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}" restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.repo-init.outputs.result }}'
backup-id: ${{ steps.exchange-backup.outputs.backup-id }} backup-id: ${{ steps.exchange-backup.outputs.backup-id }}
log-dir: ${{ env.CORSO_LOG_DIR }} log-dir: ${{ env.CORSO_LOG_DIR }}
with-export: true with-export: true
########################################################################################################################################## ##########################################################################################################################################
# Onedrive # Onedrive
@ -274,8 +270,8 @@ jobs:
service: onedrive service: onedrive
kind: first-backup kind: first-backup
backup-args: '--user "${{ env.TEST_USER }}"' backup-args: '--user "${{ env.TEST_USER }}"'
restore-args: "--folder ${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-onedrive.outputs.result }}" restore-args: '--folder ${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-onedrive.outputs.result }}'
restore-container: "${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-onedrive.outputs.result }}" restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-onedrive.outputs.result }}'
log-dir: ${{ env.CORSO_LOG_DIR }} log-dir: ${{ env.CORSO_LOG_DIR }}
with-export: true with-export: true
@ -299,8 +295,8 @@ jobs:
service: onedrive service: onedrive
kind: incremental kind: incremental
backup-args: '--user "${{ env.TEST_USER }}"' backup-args: '--user "${{ env.TEST_USER }}"'
restore-args: "--folder ${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-onedrive.outputs.result }}" restore-args: '--folder ${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-onedrive.outputs.result }}'
restore-container: "${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-onedrive.outputs.result }}" restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-onedrive.outputs.result }}'
log-dir: ${{ env.CORSO_LOG_DIR }} log-dir: ${{ env.CORSO_LOG_DIR }}
with-export: true with-export: true
@ -334,8 +330,8 @@ jobs:
service: sharepoint service: sharepoint
kind: first-backup kind: first-backup
backup-args: '--site "${{ vars.CORSO_M365_TEST_SITE_URL }}" --data libraries' backup-args: '--site "${{ vars.CORSO_M365_TEST_SITE_URL }}" --data libraries'
restore-args: "--folder ${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-sharepoint.outputs.result }}" restore-args: '--folder ${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-sharepoint.outputs.result }}'
restore-container: "${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-sharepoint.outputs.result }}" restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-sharepoint.outputs.result }}'
log-dir: ${{ env.CORSO_LOG_DIR }} log-dir: ${{ env.CORSO_LOG_DIR }}
with-export: true with-export: true
category: libraries category: libraries
@ -361,8 +357,8 @@ jobs:
service: sharepoint service: sharepoint
kind: incremental kind: incremental
backup-args: '--site "${{ vars.CORSO_M365_TEST_SITE_URL }}" --data libraries' backup-args: '--site "${{ vars.CORSO_M365_TEST_SITE_URL }}" --data libraries'
restore-args: "--folder ${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-sharepoint.outputs.result }}" restore-args: '--folder ${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-sharepoint.outputs.result }}'
restore-container: "${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-sharepoint.outputs.result }}" restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-sharepoint.outputs.result }}'
log-dir: ${{ env.CORSO_LOG_DIR }} log-dir: ${{ env.CORSO_LOG_DIR }}
with-export: true with-export: true
category: libraries category: libraries
@ -487,8 +483,8 @@ jobs:
with: with:
service: groups service: groups
kind: first-backup kind: first-backup
backup-args: '--group "${{ vars.CORSO_M365_TEST_TEAM_ID }}" --data messages,libraries' backup-args: '--group "${{ vars.CORSO_M365_TEST_TEAM_ID }}"'
restore-container: "${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-groups.outputs.result }}" restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-groups.outputs.result }}'
log-dir: ${{ env.CORSO_LOG_DIR }} log-dir: ${{ env.CORSO_LOG_DIR }}
with-export: true with-export: true
@ -512,9 +508,9 @@ jobs:
with: with:
service: groups service: groups
kind: incremental kind: incremental
backup-args: '--group "${{ vars.CORSO_M365_TEST_TEAM_ID }}" --data messages,libraries' backup-args: '--group "${{ vars.CORSO_M365_TEST_TEAM_ID }}"'
restore-args: '--site "${{ vars.CORSO_M365_TEST_GROUPS_SITE_URL }}" --folder ${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-groups.outputs.result }}' restore-args: '--site "${{ vars.CORSO_M365_TEST_GROUPS_SITE_URL }}" --folder ${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-groups.outputs.result }}'
restore-container: "${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-groups.outputs.result }}" restore-container: '${{ env.RESTORE_DEST_PFX }}${{ steps.new-data-creation-groups.outputs.result }}'
log-dir: ${{ env.CORSO_LOG_DIR }} log-dir: ${{ env.CORSO_LOG_DIR }}
with-export: true with-export: true
@ -536,5 +532,5 @@ jobs:
if: failure() if: failure()
uses: ./.github/actions/teams-message uses: ./.github/actions/teams-message
with: with:
msg: "[CORSO FAILED] Sanity Tests" msg: "[FAILED] Sanity Tests"
teams_url: ${{ secrets.TEAMS_CORSO_CI_WEBHOOK_URL }} teams_url: ${{ secrets.TEAMS_CORSO_CI_WEBHOOK_URL }}

View File

@ -6,22 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] (beta) ## [Unreleased] (beta)
### Fixed
- Handle the case where an email or event cannot be retrieved from Exchange due to an `ErrorCorruptData` error. Corso will skip over the item but report it in the backup summary.
- Emails attached within other emails are now correctly exported
- Gracefully handle email and post attachments without name when exporting to eml
- Use correct timezone for event start and end times in Exchange exports (helps fix issues in relative recurrence patterns)
- Fixed an issue causing exports dealing with calendar data to have high memory usage
## [v0.19.0] (beta) - 2024-02-06
### Added ### Added
- Events can now be exported from Exchange backups as .ics files. - Events can now be exported from Exchange backups as .ics files.
- Update repo init configuration to reduce the total number of GET requests sent - Update repo init configuration to reduce the total number of GET requests sent
to the object store when using corso. This affects repos that have many to the object store when using corso. This affects repos that have many
backups created in them per day the most. backups created in them per day the most.
- Feature Preview: Corso now supports backup, export & restore of SharePoint lists. Lists backup can be initiated using `corso backup create sharepoint --site <site-url> --data lists`. - Group mailbox emails can now be exported as `.eml` files.
- Group mailbox(aka conversations) backup and export support is now officially available. Group mailbox posts can be exported as `.eml` files.
### Fixed ### Fixed
- Retry transient 400 "invalidRequest" errors during onedrive & sharepoint backup. - Retry transient 400 "invalidRequest" errors during onedrive & sharepoint backup.
@ -42,10 +33,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Exchange in-place restore may restore items in well-known folders to different folders if the user has well-known folder names change based on locale and has updated the locale since the backup was created. - Exchange in-place restore may restore items in well-known folders to different folders if the user has well-known folder names change based on locale and has updated the locale since the backup was created.
- In-place Exchange contacts restore will merge items in folders named "Contacts" or "contacts" into the default folder. - In-place Exchange contacts restore will merge items in folders named "Contacts" or "contacts" into the default folder.
- External users with access through shared links will not receive these links as they are not sent via email during restore. - External users with access through shared links will not receive these links as they are not sent via email during restore.
- Graph API has limited support for certain column types such as `location`, `hyperlink/picture`, and `metadata`. Restoring SharePoint list items containing these columns will result in differences compared to the original items.
- SharePoint list item attachments are not available due to graph API limitations.
- Group mailbox restore is not supported due to limited Graph API support for creating mailbox items.
- Due to Graph API limitations, any group mailbox items present in subfolders other than Inbox aren't backed up.
## [v0.18.0] (beta) - 2024-01-02 ## [v0.18.0] (beta) - 2024-01-02
@ -502,8 +489,7 @@ this case, Corso will skip over the item but report this in the backup summary.
- Miscellaneous - Miscellaneous
- Optional usage statistics reporting ([RM-35](https://github.com/alcionai/corso-roadmap/issues/35)) - Optional usage statistics reporting ([RM-35](https://github.com/alcionai/corso-roadmap/issues/35))
[Unreleased]: https://github.com/alcionai/corso/compare/v0.19.0...HEAD [Unreleased]: https://github.com/alcionai/corso/compare/v0.18.0...HEAD
[v0.19.0]: https://github.com/alcionai/corso/compare/v0.18.0...v0.19.0
[v0.18.0]: https://github.com/alcionai/corso/compare/v0.17.0...v0.18.0 [v0.18.0]: https://github.com/alcionai/corso/compare/v0.17.0...v0.18.0
[v0.17.0]: https://github.com/alcionai/corso/compare/v0.16.0...v0.17.0 [v0.17.0]: https://github.com/alcionai/corso/compare/v0.16.0...v0.17.0
[v0.16.0]: https://github.com/alcionai/corso/compare/v0.15.0...v0.16.0 [v0.16.0]: https://github.com/alcionai/corso/compare/v0.15.0...v0.16.0

View File

@ -1,6 +1,3 @@
> [!NOTE]
> **The Corso project is no longer actively maintained and has been archived**.
<p align="center"> <p align="center">
<img src="https://github.com/alcionai/corso/blob/main/website/static/img/corso_logo.svg?raw=true" alt="Corso Logo" width="100" /> <img src="https://github.com/alcionai/corso/blob/main/website/static/img/corso_logo.svg?raw=true" alt="Corso Logo" width="100" />
</p> </p>

View File

@ -18,7 +18,6 @@ import (
"github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/internal/operations" "github.com/alcionai/corso/src/internal/operations"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/config" "github.com/alcionai/corso/src/pkg/config"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
@ -40,7 +39,7 @@ var (
type NoBackupExchangeE2ESuite struct { type NoBackupExchangeE2ESuite struct {
tester.Suite tester.Suite
dpnd dependencies dpnd dependencies
m365 its.M365IntgTestSetup its intgTesterSetup
} }
func TestNoBackupExchangeE2ESuite(t *testing.T) { func TestNoBackupExchangeE2ESuite(t *testing.T) {
@ -55,7 +54,7 @@ func (suite *NoBackupExchangeE2ESuite) SetupSuite() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
suite.m365 = its.GetM365(t) suite.its = newIntegrationTesterSetup(t)
suite.dpnd = prepM365Test(t, ctx, path.ExchangeService) suite.dpnd = prepM365Test(t, ctx, path.ExchangeService)
} }
@ -94,7 +93,7 @@ func (suite *NoBackupExchangeE2ESuite) TestExchangeBackupListCmd_noBackups() {
type BackupExchangeE2ESuite struct { type BackupExchangeE2ESuite struct {
tester.Suite tester.Suite
dpnd dependencies dpnd dependencies
m365 its.M365IntgTestSetup its intgTesterSetup
} }
func TestBackupExchangeE2ESuite(t *testing.T) { func TestBackupExchangeE2ESuite(t *testing.T) {
@ -109,7 +108,7 @@ func (suite *BackupExchangeE2ESuite) SetupSuite() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
suite.m365 = its.GetM365(t) suite.its = newIntegrationTesterSetup(t)
suite.dpnd = prepM365Test(t, ctx, path.ExchangeService) suite.dpnd = prepM365Test(t, ctx, path.ExchangeService)
} }
@ -139,7 +138,7 @@ func runExchangeBackupCategoryTest(suite *BackupExchangeE2ESuite, category path.
cmd, ctx := buildExchangeBackupCmd( cmd, ctx := buildExchangeBackupCmd(
ctx, ctx,
suite.dpnd.configFilePath, suite.dpnd.configFilePath,
suite.m365.User.ID, suite.its.user.ID,
category.String(), category.String(),
&recorder) &recorder)
@ -150,11 +149,8 @@ func runExchangeBackupCategoryTest(suite *BackupExchangeE2ESuite, category path.
result := recorder.String() result := recorder.String()
t.Log("backup results", result) t.Log("backup results", result)
// As an offhand check: the result should contain the m365 user's email. // as an offhand check: the result should contain the m365 user id
assert.Contains( assert.Contains(t, result, suite.its.user.ID)
t,
strings.ToLower(result),
strings.ToLower(suite.m365.User.Provider.Name()))
} }
func (suite *BackupExchangeE2ESuite) TestExchangeBackupCmd_ServiceNotEnabled_email() { func (suite *BackupExchangeE2ESuite) TestExchangeBackupCmd_ServiceNotEnabled_email() {
@ -177,7 +173,7 @@ func runExchangeBackupServiceNotEnabledTest(suite *BackupExchangeE2ESuite, categ
cmd, ctx := buildExchangeBackupCmd( cmd, ctx := buildExchangeBackupCmd(
ctx, ctx,
suite.dpnd.configFilePath, suite.dpnd.configFilePath,
fmt.Sprintf("%s,%s", tconfig.UnlicensedM365UserID(suite.T()), suite.m365.User.ID), fmt.Sprintf("%s,%s", tconfig.UnlicensedM365UserID(suite.T()), suite.its.user.ID),
category.String(), category.String(),
&recorder) &recorder)
err := cmd.ExecuteContext(ctx) err := cmd.ExecuteContext(ctx)
@ -186,11 +182,8 @@ func runExchangeBackupServiceNotEnabledTest(suite *BackupExchangeE2ESuite, categ
result := recorder.String() result := recorder.String()
t.Log("backup results", result) t.Log("backup results", result)
// As an offhand check: the result should contain the m365 user's email. // as an offhand check: the result should contain the m365 user id
assert.Contains( assert.Contains(t, result, suite.its.user.ID)
t,
strings.ToLower(result),
strings.ToLower(suite.m365.User.Provider.Name()))
} }
func (suite *BackupExchangeE2ESuite) TestExchangeBackupCmd_userNotFound_email() { func (suite *BackupExchangeE2ESuite) TestExchangeBackupCmd_userNotFound_email() {
@ -249,7 +242,7 @@ func (suite *BackupExchangeE2ESuite) TestBackupCreateExchange_badAzureClientIDFl
cmd := cliTD.StubRootCmd( cmd := cliTD.StubRootCmd(
"backup", "create", "exchange", "backup", "create", "exchange",
"--user", suite.m365.User.ID, "--user", suite.its.user.ID,
"--azure-client-id", "invalid-value") "--azure-client-id", "invalid-value")
cli.BuildCommandTree(cmd) cli.BuildCommandTree(cmd)
@ -273,7 +266,7 @@ func (suite *BackupExchangeE2ESuite) TestBackupCreateExchange_fromConfigFile() {
cmd := cliTD.StubRootCmd( cmd := cliTD.StubRootCmd(
"backup", "create", "exchange", "backup", "create", "exchange",
"--user", suite.m365.User.ID, "--user", suite.its.user.ID,
"--"+flags.ConfigFileFN, suite.dpnd.configFilePath) "--"+flags.ConfigFileFN, suite.dpnd.configFilePath)
cli.BuildCommandTree(cmd) cli.BuildCommandTree(cmd)
@ -288,11 +281,8 @@ func (suite *BackupExchangeE2ESuite) TestBackupCreateExchange_fromConfigFile() {
result := suite.dpnd.recorder.String() result := suite.dpnd.recorder.String()
t.Log("backup results", result) t.Log("backup results", result)
// As an offhand check: the result should contain the m365 user's email. // as an offhand check: the result should contain the m365 user id
assert.Contains( assert.Contains(t, result, suite.its.user.ID)
t,
strings.ToLower(result),
strings.ToLower(suite.m365.User.Provider.Name()))
} }
// AWS flags // AWS flags
@ -306,7 +296,7 @@ func (suite *BackupExchangeE2ESuite) TestBackupCreateExchange_badAWSFlags() {
cmd := cliTD.StubRootCmd( cmd := cliTD.StubRootCmd(
"backup", "create", "exchange", "backup", "create", "exchange",
"--user", suite.m365.User.ID, "--user", suite.its.user.ID,
"--aws-access-key", "invalid-value", "--aws-access-key", "invalid-value",
"--aws-secret-access-key", "some-invalid-value") "--aws-secret-access-key", "some-invalid-value")
cli.BuildCommandTree(cmd) cli.BuildCommandTree(cmd)
@ -329,7 +319,7 @@ type PreparedBackupExchangeE2ESuite struct {
tester.Suite tester.Suite
dpnd dependencies dpnd dependencies
backupOps map[path.CategoryType]string backupOps map[path.CategoryType]string
m365 its.M365IntgTestSetup its intgTesterSetup
} }
func TestPreparedBackupExchangeE2ESuite(t *testing.T) { func TestPreparedBackupExchangeE2ESuite(t *testing.T) {
@ -346,13 +336,13 @@ func (suite *PreparedBackupExchangeE2ESuite) SetupSuite() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
suite.m365 = its.GetM365(t) suite.its = newIntegrationTesterSetup(t)
suite.dpnd = prepM365Test(t, ctx, path.ExchangeService) suite.dpnd = prepM365Test(t, ctx, path.ExchangeService)
suite.backupOps = make(map[path.CategoryType]string) suite.backupOps = make(map[path.CategoryType]string)
var ( var (
users = []string{suite.m365.User.ID} users = []string{suite.its.user.ID}
ins = idname.NewCache(map[string]string{suite.m365.User.ID: suite.m365.User.ID}) ins = idname.NewCache(map[string]string{suite.its.user.ID: suite.its.user.ID})
) )
for _, set := range []path.CategoryType{email, contacts, events} { for _, set := range []path.CategoryType{email, contacts, events} {

View File

@ -35,12 +35,9 @@ const (
groupsServiceCommandCreateExamples = `# Backup all Groups and Teams data for the Marketing group groupsServiceCommandCreateExamples = `# Backup all Groups and Teams data for the Marketing group
corso backup create groups --group Marketing corso backup create groups --group Marketing
# Backup only Teams channel messages # Backup only Teams conversations messages
corso backup create groups --group Marketing --data messages corso backup create groups --group Marketing --data messages
# Backup only group mailbox posts
corso backup create groups --group Marketing --data conversations
# Backup all Groups and Teams data for all groups # Backup all Groups and Teams data for all groups
corso backup create groups --group '*'` corso backup create groups --group '*'`
@ -53,10 +50,7 @@ corso backup details groups --backup 1234abcd-12ab-cd34-56de-1234abcd
# Explore Marketing messages posted after the start of 2022 # Explore Marketing messages posted after the start of 2022
corso backup details groups --backup 1234abcd-12ab-cd34-56de-1234abcd \ corso backup details groups --backup 1234abcd-12ab-cd34-56de-1234abcd \
--last-message-reply-after 2022-01-01T00:00:00 --last-message-reply-after 2022-01-01T00:00:00`
# Explore group mailbox posts with conversation subject "hello world"
corso backup details groups --backup 1234abcd-12ab-cd34-56de-1234abcd --conversation "hello world"`
) )
// called by backup.go to map subcommands to provider-specific handling. // called by backup.go to map subcommands to provider-specific handling.

View File

@ -20,7 +20,6 @@ import (
"github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/internal/operations" "github.com/alcionai/corso/src/internal/operations"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/config" "github.com/alcionai/corso/src/pkg/config"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
@ -36,7 +35,7 @@ import (
type NoBackupGroupsE2ESuite struct { type NoBackupGroupsE2ESuite struct {
tester.Suite tester.Suite
dpnd dependencies dpnd dependencies
m365 its.M365IntgTestSetup its intgTesterSetup
} }
func TestNoBackupGroupsE2ESuite(t *testing.T) { func TestNoBackupGroupsE2ESuite(t *testing.T) {
@ -51,7 +50,7 @@ func (suite *NoBackupGroupsE2ESuite) SetupSuite() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
suite.m365 = its.GetM365(t) suite.its = newIntegrationTesterSetup(t)
suite.dpnd = prepM365Test(t, ctx, path.GroupsService) suite.dpnd = prepM365Test(t, ctx, path.GroupsService)
} }
@ -90,7 +89,7 @@ func (suite *NoBackupGroupsE2ESuite) TestGroupsBackupListCmd_noBackups() {
type BackupGroupsE2ESuite struct { type BackupGroupsE2ESuite struct {
tester.Suite tester.Suite
dpnd dependencies dpnd dependencies
m365 its.M365IntgTestSetup its intgTesterSetup
} }
func TestBackupGroupsE2ESuite(t *testing.T) { func TestBackupGroupsE2ESuite(t *testing.T) {
@ -105,7 +104,7 @@ func (suite *BackupGroupsE2ESuite) SetupSuite() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
suite.m365 = its.GetM365(t) suite.its = newIntegrationTesterSetup(t)
suite.dpnd = prepM365Test(t, ctx, path.GroupsService) suite.dpnd = prepM365Test(t, ctx, path.GroupsService)
} }
@ -114,8 +113,6 @@ func (suite *BackupGroupsE2ESuite) TestGroupsBackupCmd_channelMessages() {
} }
func (suite *BackupGroupsE2ESuite) TestGroupsBackupCmd_conversations() { func (suite *BackupGroupsE2ESuite) TestGroupsBackupCmd_conversations() {
// skip
suite.T().Skip("CorsoCITeam group mailbox backup is broken")
runGroupsBackupCategoryTest(suite, flags.DataConversations) runGroupsBackupCategoryTest(suite, flags.DataConversations)
} }
@ -137,7 +134,7 @@ func runGroupsBackupCategoryTest(suite *BackupGroupsE2ESuite, category string) {
cmd, ctx := buildGroupsBackupCmd( cmd, ctx := buildGroupsBackupCmd(
ctx, ctx,
suite.dpnd.configFilePath, suite.dpnd.configFilePath,
suite.m365.Group.ID, suite.its.group.ID,
category, category,
&recorder) &recorder)
@ -205,7 +202,7 @@ func (suite *BackupGroupsE2ESuite) TestBackupCreateGroups_badAzureClientIDFlag()
cmd := cliTD.StubRootCmd( cmd := cliTD.StubRootCmd(
"backup", "create", "groups", "backup", "create", "groups",
"--group", suite.m365.Group.ID, "--group", suite.its.group.ID,
"--azure-client-id", "invalid-value") "--azure-client-id", "invalid-value")
cli.BuildCommandTree(cmd) cli.BuildCommandTree(cmd)
@ -219,9 +216,6 @@ func (suite *BackupGroupsE2ESuite) TestBackupCreateGroups_badAzureClientIDFlag()
} }
func (suite *BackupGroupsE2ESuite) TestBackupCreateGroups_fromConfigFile() { func (suite *BackupGroupsE2ESuite) TestBackupCreateGroups_fromConfigFile() {
// Skip
suite.T().Skip("CorsoCITeam group mailbox backup is broken")
t := suite.T() t := suite.T()
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
ctx = config.SetViper(ctx, suite.dpnd.vpr) ctx = config.SetViper(ctx, suite.dpnd.vpr)
@ -232,7 +226,7 @@ func (suite *BackupGroupsE2ESuite) TestBackupCreateGroups_fromConfigFile() {
cmd := cliTD.StubRootCmd( cmd := cliTD.StubRootCmd(
"backup", "create", "groups", "backup", "create", "groups",
"--group", suite.m365.Group.ID, "--group", suite.its.group.ID,
"--"+flags.ConfigFileFN, suite.dpnd.configFilePath) "--"+flags.ConfigFileFN, suite.dpnd.configFilePath)
cli.BuildCommandTree(cmd) cli.BuildCommandTree(cmd)
@ -256,7 +250,7 @@ func (suite *BackupGroupsE2ESuite) TestBackupCreateGroups_badAWSFlags() {
cmd := cliTD.StubRootCmd( cmd := cliTD.StubRootCmd(
"backup", "create", "groups", "backup", "create", "groups",
"--group", suite.m365.Group.ID, "--group", suite.its.group.ID,
"--aws-access-key", "invalid-value", "--aws-access-key", "invalid-value",
"--aws-secret-access-key", "some-invalid-value") "--aws-secret-access-key", "some-invalid-value")
cli.BuildCommandTree(cmd) cli.BuildCommandTree(cmd)
@ -279,7 +273,7 @@ type PreparedBackupGroupsE2ESuite struct {
tester.Suite tester.Suite
dpnd dependencies dpnd dependencies
backupOps map[path.CategoryType]string backupOps map[path.CategoryType]string
m365 its.M365IntgTestSetup its intgTesterSetup
} }
func TestPreparedBackupGroupsE2ESuite(t *testing.T) { func TestPreparedBackupGroupsE2ESuite(t *testing.T) {
@ -296,19 +290,16 @@ func (suite *PreparedBackupGroupsE2ESuite) SetupSuite() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
suite.m365 = its.GetM365(t) suite.its = newIntegrationTesterSetup(t)
suite.dpnd = prepM365Test(t, ctx, path.GroupsService) suite.dpnd = prepM365Test(t, ctx, path.GroupsService)
suite.backupOps = make(map[path.CategoryType]string) suite.backupOps = make(map[path.CategoryType]string)
var ( var (
groups = []string{suite.m365.Group.ID} groups = []string{suite.its.group.ID}
ins = idname.NewCache(map[string]string{suite.m365.Group.ID: suite.m365.Group.ID}) ins = idname.NewCache(map[string]string{suite.its.group.ID: suite.its.group.ID})
cats = []path.CategoryType{ cats = []path.CategoryType{
path.ChannelMessagesCategory, path.ChannelMessagesCategory,
// TODO(pandeyabs): CorsoCITeam group mailbox backup is currently broken because of invalid path.ConversationPostsCategory,
// odata.NextLink which causes an infinite loop during paging. Disabling conversations tests while
// we go fix the group mailbox.
// path.ConversationPostsCategory,
path.LibrariesCategory, path.LibrariesCategory,
} }
) )
@ -462,8 +453,6 @@ func (suite *PreparedBackupGroupsE2ESuite) TestGroupsDetailsCmd_channelMessages(
} }
func (suite *PreparedBackupGroupsE2ESuite) TestGroupsDetailsCmd_conversations() { func (suite *PreparedBackupGroupsE2ESuite) TestGroupsDetailsCmd_conversations() {
// skip
suite.T().Skip("CorsoCITeam group mailbox backup is broken")
runGroupsDetailsCmdTest(suite, path.ConversationPostsCategory) runGroupsDetailsCmdTest(suite, path.ConversationPostsCategory)
} }

View File

@ -14,16 +14,141 @@ import (
"github.com/alcionai/corso/src/cli/flags" "github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/cli/print" "github.com/alcionai/corso/src/cli/print"
cliTD "github.com/alcionai/corso/src/cli/testdata" cliTD "github.com/alcionai/corso/src/cli/testdata"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/config" "github.com/alcionai/corso/src/pkg/config"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/count"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/repository" "github.com/alcionai/corso/src/pkg/repository"
"github.com/alcionai/corso/src/pkg/services/m365/api"
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
"github.com/alcionai/corso/src/pkg/storage" "github.com/alcionai/corso/src/pkg/storage"
"github.com/alcionai/corso/src/pkg/storage/testdata" "github.com/alcionai/corso/src/pkg/storage/testdata"
) )
// ---------------------------------------------------------------------------
// Gockable client
// ---------------------------------------------------------------------------
// GockClient produces a new exchange api client that can be
// mocked using gock.
func gockClient(creds account.M365Config, counter *count.Bus) (api.Client, error) {
s, err := graph.NewGockService(creds, counter)
if err != nil {
return api.Client{}, err
}
li, err := graph.NewGockService(creds, counter, graph.NoTimeout())
if err != nil {
return api.Client{}, err
}
return api.Client{
Credentials: creds,
Stable: s,
LargeItem: li,
}, nil
}
// ---------------------------------------------------------------------------
// Suite Setup
// ---------------------------------------------------------------------------
type ids struct {
ID string
DriveID string
DriveRootFolderID string
}
type intgTesterSetup struct {
acct account.Account
ac api.Client
gockAC api.Client
user ids
site ids
group ids
team ids
}
func newIntegrationTesterSetup(t *testing.T) intgTesterSetup {
its := intgTesterSetup{}
ctx, flush := tester.NewContext(t)
defer flush()
graph.InitializeConcurrencyLimiter(ctx, true, 4)
its.acct = tconfig.NewM365Account(t)
creds, err := its.acct.M365Config()
require.NoError(t, err, clues.ToCore(err))
its.ac, err = api.NewClient(
creds,
control.DefaultOptions(),
count.New())
require.NoError(t, err, clues.ToCore(err))
its.gockAC, err = gockClient(creds, count.New())
require.NoError(t, err, clues.ToCore(err))
// user drive
uids := ids{}
uids.ID = tconfig.M365UserID(t)
userDrive, err := its.ac.Users().GetDefaultDrive(ctx, uids.ID)
require.NoError(t, err, clues.ToCore(err))
uids.DriveID = ptr.Val(userDrive.GetId())
userDriveRootFolder, err := its.ac.Drives().GetRootFolder(ctx, uids.DriveID)
require.NoError(t, err, clues.ToCore(err))
uids.DriveRootFolderID = ptr.Val(userDriveRootFolder.GetId())
its.user = uids
// site
sids := ids{}
sids.ID = tconfig.M365SiteID(t)
siteDrive, err := its.ac.Sites().GetDefaultDrive(ctx, sids.ID)
require.NoError(t, err, clues.ToCore(err))
sids.DriveID = ptr.Val(siteDrive.GetId())
siteDriveRootFolder, err := its.ac.Drives().GetRootFolder(ctx, sids.DriveID)
require.NoError(t, err, clues.ToCore(err))
sids.DriveRootFolderID = ptr.Val(siteDriveRootFolder.GetId())
its.site = sids
// group
gids := ids{}
// use of the TeamID is intentional here, so that we are assured
// the group has full usage of the teams api.
gids.ID = tconfig.M365TeamID(t)
its.group = gids
// team
tids := ids{}
tids.ID = tconfig.M365TeamID(t)
its.team = tids
return its
}
type dependencies struct { type dependencies struct {
st storage.Storage st storage.Storage
repo repository.Repositoryer repo repository.Repositoryer

View File

@ -20,7 +20,6 @@ import (
"github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/internal/operations" "github.com/alcionai/corso/src/internal/operations"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/config" "github.com/alcionai/corso/src/pkg/config"
@ -90,7 +89,7 @@ func (suite *NoBackupSharePointE2ESuite) TestSharePointBackupListCmd_empty() {
type BackupSharepointE2ESuite struct { type BackupSharepointE2ESuite struct {
tester.Suite tester.Suite
dpnd dependencies dpnd dependencies
m365 its.M365IntgTestSetup its intgTesterSetup
} }
func TestBackupSharepointE2ESuite(t *testing.T) { func TestBackupSharepointE2ESuite(t *testing.T) {
@ -105,7 +104,7 @@ func (suite *BackupSharepointE2ESuite) SetupSuite() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
suite.m365 = its.GetM365(t) suite.its = newIntegrationTesterSetup(t)
suite.dpnd = prepM365Test(t, ctx, path.SharePointService) suite.dpnd = prepM365Test(t, ctx, path.SharePointService)
} }
@ -129,7 +128,7 @@ func runSharepointBackupCategoryTest(suite *BackupSharepointE2ESuite, category s
cmd, ctx := buildSharepointBackupCmd( cmd, ctx := buildSharepointBackupCmd(
ctx, ctx,
suite.dpnd.configFilePath, suite.dpnd.configFilePath,
suite.m365.Site.ID, suite.its.site.ID,
category, category,
&recorder) &recorder)
@ -188,7 +187,7 @@ type PreparedBackupSharepointE2ESuite struct {
tester.Suite tester.Suite
dpnd dependencies dpnd dependencies
backupOps map[path.CategoryType]string backupOps map[path.CategoryType]string
m365 its.M365IntgTestSetup its intgTesterSetup
} }
func TestPreparedBackupSharepointE2ESuite(t *testing.T) { func TestPreparedBackupSharepointE2ESuite(t *testing.T) {
@ -205,13 +204,13 @@ func (suite *PreparedBackupSharepointE2ESuite) SetupSuite() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
suite.m365 = its.GetM365(t) suite.its = newIntegrationTesterSetup(t)
suite.dpnd = prepM365Test(t, ctx, path.SharePointService) suite.dpnd = prepM365Test(t, ctx, path.SharePointService)
suite.backupOps = make(map[path.CategoryType]string) suite.backupOps = make(map[path.CategoryType]string)
var ( var (
sites = []string{suite.m365.Site.ID} sites = []string{suite.its.site.ID}
ins = idname.NewCache(map[string]string{suite.m365.Site.ID: suite.m365.Site.ID}) ins = idname.NewCache(map[string]string{suite.its.site.ID: suite.its.site.ID})
cats = []path.CategoryType{ cats = []path.CategoryType{
path.ListsCategory, path.ListsCategory,
} }

View File

@ -20,7 +20,6 @@ import (
"github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/internal/operations" "github.com/alcionai/corso/src/internal/operations"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/config" "github.com/alcionai/corso/src/pkg/config"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
@ -36,7 +35,7 @@ import (
type NoBackupTeamsChatsE2ESuite struct { type NoBackupTeamsChatsE2ESuite struct {
tester.Suite tester.Suite
dpnd dependencies dpnd dependencies
m365 its.M365IntgTestSetup its intgTesterSetup
} }
func TestNoBackupTeamsChatsE2ESuite(t *testing.T) { func TestNoBackupTeamsChatsE2ESuite(t *testing.T) {
@ -47,12 +46,11 @@ func TestNoBackupTeamsChatsE2ESuite(t *testing.T) {
func (suite *NoBackupTeamsChatsE2ESuite) SetupSuite() { func (suite *NoBackupTeamsChatsE2ESuite) SetupSuite() {
t := suite.T() t := suite.T()
t.Skip("not fully implemented")
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
suite.m365 = its.GetM365(t) suite.its = newIntegrationTesterSetup(t)
suite.dpnd = prepM365Test(t, ctx, path.TeamsChatsService) suite.dpnd = prepM365Test(t, ctx, path.TeamsChatsService)
} }
@ -91,7 +89,7 @@ func (suite *NoBackupTeamsChatsE2ESuite) TestTeamsChatsBackupListCmd_noBackups()
type BackupTeamsChatsE2ESuite struct { type BackupTeamsChatsE2ESuite struct {
tester.Suite tester.Suite
dpnd dependencies dpnd dependencies
m365 its.M365IntgTestSetup its intgTesterSetup
} }
func TestBackupTeamsChatsE2ESuite(t *testing.T) { func TestBackupTeamsChatsE2ESuite(t *testing.T) {
@ -102,12 +100,11 @@ func TestBackupTeamsChatsE2ESuite(t *testing.T) {
func (suite *BackupTeamsChatsE2ESuite) SetupSuite() { func (suite *BackupTeamsChatsE2ESuite) SetupSuite() {
t := suite.T() t := suite.T()
t.Skip("not fully implemented")
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
suite.m365 = its.GetM365(t) suite.its = newIntegrationTesterSetup(t)
suite.dpnd = prepM365Test(t, ctx, path.TeamsChatsService) suite.dpnd = prepM365Test(t, ctx, path.TeamsChatsService)
} }
@ -129,7 +126,7 @@ func runTeamsChatsBackupCategoryTest(suite *BackupTeamsChatsE2ESuite, category s
cmd, ctx := buildTeamsChatsBackupCmd( cmd, ctx := buildTeamsChatsBackupCmd(
ctx, ctx,
suite.dpnd.configFilePath, suite.dpnd.configFilePath,
suite.m365.User.ID, suite.its.user.ID,
category, category,
&recorder) &recorder)
@ -189,7 +186,7 @@ func (suite *BackupTeamsChatsE2ESuite) TestBackupCreateTeamsChats_badAzureClient
cmd := cliTD.StubRootCmd( cmd := cliTD.StubRootCmd(
"backup", "create", "chats", "backup", "create", "chats",
"--teamschat", suite.m365.User.ID, "--teamschat", suite.its.user.ID,
"--azure-client-id", "invalid-value") "--azure-client-id", "invalid-value")
cli.BuildCommandTree(cmd) cli.BuildCommandTree(cmd)
@ -213,7 +210,7 @@ func (suite *BackupTeamsChatsE2ESuite) TestBackupCreateTeamsChats_fromConfigFile
cmd := cliTD.StubRootCmd( cmd := cliTD.StubRootCmd(
"backup", "create", "chats", "backup", "create", "chats",
"--teamschat", suite.m365.User.ID, "--teamschat", suite.its.user.ID,
"--"+flags.ConfigFileFN, suite.dpnd.configFilePath) "--"+flags.ConfigFileFN, suite.dpnd.configFilePath)
cli.BuildCommandTree(cmd) cli.BuildCommandTree(cmd)
@ -237,7 +234,7 @@ func (suite *BackupTeamsChatsE2ESuite) TestBackupCreateTeamsChats_badAWSFlags()
cmd := cliTD.StubRootCmd( cmd := cliTD.StubRootCmd(
"backup", "create", "chats", "backup", "create", "chats",
"--teamschat", suite.m365.User.ID, "--teamschat", suite.its.user.ID,
"--aws-access-key", "invalid-value", "--aws-access-key", "invalid-value",
"--aws-secret-access-key", "some-invalid-value") "--aws-secret-access-key", "some-invalid-value")
cli.BuildCommandTree(cmd) cli.BuildCommandTree(cmd)
@ -260,7 +257,7 @@ type PreparedBackupTeamsChatsE2ESuite struct {
tester.Suite tester.Suite
dpnd dependencies dpnd dependencies
backupOps map[path.CategoryType]string backupOps map[path.CategoryType]string
m365 its.M365IntgTestSetup its intgTesterSetup
} }
func TestPreparedBackupTeamsChatsE2ESuite(t *testing.T) { func TestPreparedBackupTeamsChatsE2ESuite(t *testing.T) {
@ -273,18 +270,17 @@ func TestPreparedBackupTeamsChatsE2ESuite(t *testing.T) {
func (suite *PreparedBackupTeamsChatsE2ESuite) SetupSuite() { func (suite *PreparedBackupTeamsChatsE2ESuite) SetupSuite() {
t := suite.T() t := suite.T()
t.Skip("not fully implemented")
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
suite.m365 = its.GetM365(t) suite.its = newIntegrationTesterSetup(t)
suite.dpnd = prepM365Test(t, ctx, path.TeamsChatsService) suite.dpnd = prepM365Test(t, ctx, path.TeamsChatsService)
suite.backupOps = make(map[path.CategoryType]string) suite.backupOps = make(map[path.CategoryType]string)
var ( var (
teamschats = []string{suite.m365.User.ID} teamschats = []string{suite.its.user.ID}
ins = idname.NewCache(map[string]string{suite.m365.User.ID: suite.m365.User.ID}) ins = idname.NewCache(map[string]string{suite.its.user.ID: suite.its.user.ID})
cats = []path.CategoryType{ cats = []path.CategoryType{
path.ChatsCategory, path.ChatsCategory,
} }
@ -491,7 +487,6 @@ func TestBackupDeleteTeamsChatsE2ESuite(t *testing.T) {
func (suite *BackupDeleteTeamsChatsE2ESuite) SetupSuite() { func (suite *BackupDeleteTeamsChatsE2ESuite) SetupSuite() {
t := suite.T() t := suite.T()
t.Skip("not fully implemented")
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()

View File

@ -50,13 +50,7 @@ corso export groups my-exports --backup 1234abcd-12ab-cd34-56de-1234abcd
# Export all files and folders in folder "Documents/Finance Reports" that were created before 2020 to /my-exports # Export all files and folders in folder "Documents/Finance Reports" that were created before 2020 to /my-exports
corso export groups my-exports --backup 1234abcd-12ab-cd34-56de-1234abcd \ corso export groups my-exports --backup 1234abcd-12ab-cd34-56de-1234abcd \
--folder "Documents/Finance Reports" --file-created-before 2020-01-01T00:00:00 --folder "Documents/Finance Reports" --file-created-before 2020-01-01T00:00:00`
# Export all posts from a conversation with topic "hello world" from group mailbox's last backup to /my-exports
corso export groups my-exports --backup 1234abcd-12ab-cd34-56de-1234abcd --conversation "hello world"
# Export post with ID 98765abcdef from a conversation from group mailbox's last backup to /my-exports
corso export groups my-exports --backup 1234abcd-12ab-cd34-56de-1234abcd --conversation "hello world" --post 98765abcdef`
) )
// `corso export groups [<flag>...] <destination>` // `corso export groups [<flag>...] <destination>`

View File

@ -266,14 +266,9 @@ func IncludeGroupsRestoreDataSelectors(ctx context.Context, opts GroupsOpts) *se
opts.Conversations = selectors.Any() opts.Conversations = selectors.Any()
} }
// if no post is specified, select all posts in the conversation
if convPosts == 0 {
opts.Posts = selectors.Any()
}
// if no post is specified, only select conversations; // if no post is specified, only select conversations;
// otherwise, look for conv/post pairs // otherwise, look for channel/message pairs
if convs == 0 { if chanMsgs == 0 {
sel.Include(sel.Conversation(opts.Conversations)) sel.Include(sel.Conversation(opts.Conversations))
} else { } else {
sel.Include(sel.ConversationPosts(opts.Conversations, opts.Posts)) sel.Include(sel.ConversationPosts(opts.Conversations, opts.Posts))

View File

@ -6,6 +6,12 @@ Param (
[Parameter(Mandatory = $False, HelpMessage = "Site for which to delete folders in SharePoint")] [Parameter(Mandatory = $False, HelpMessage = "Site for which to delete folders in SharePoint")]
[String]$Site, [String]$Site,
[Parameter(Mandatory = $False, HelpMessage = "Exchange Admin email")]
[String]$AdminUser = $ENV:M365_TENANT_ADMIN_USER,
[Parameter(Mandatory = $False, HelpMessage = "Exchange Admin password")]
[String]$AdminPwd = $ENV:M365_TENANT_ADMIN_PASSWORD,
[Parameter(Mandatory = $False, HelpMessage = "Document library root. Can add multiple comma-separated values")] [Parameter(Mandatory = $False, HelpMessage = "Document library root. Can add multiple comma-separated values")]
[String[]]$LibraryNameList = @(), [String[]]$LibraryNameList = @(),
@ -16,16 +22,7 @@ Param (
[String[]]$FolderPrefixPurgeList, [String[]]$FolderPrefixPurgeList,
[Parameter(Mandatory = $False, HelpMessage = "Delete document libraries with this prefix")] [Parameter(Mandatory = $False, HelpMessage = "Delete document libraries with this prefix")]
[String[]]$LibraryPrefixDeleteList = @(), [String[]]$LibraryPrefixDeleteList = @()
[Parameter(Mandatory = $False, HelpMessage = "Tenant domain")]
[String]$TenantDomain = $ENV:TENANT_DOMAIN,
[Parameter(Mandatory = $False, HelpMessage = "Azure ClientId")]
[String]$ClientId = $ENV:AZURE_CLIENT_ID,
[Parameter(Mandatory = $False, HelpMessage = "Azure AppCert")]
[String]$AppCert = $ENV:AZURE_APP_CERT
) )
Set-StrictMode -Version 2.0 Set-StrictMode -Version 2.0
@ -111,7 +108,6 @@ function Purge-Library {
$foldersToPurge = @() $foldersToPurge = @()
$folders = Get-PnPFolderItem -FolderSiteRelativeUrl $LibraryName -ItemType Folder $folders = Get-PnPFolderItem -FolderSiteRelativeUrl $LibraryName -ItemType Folder
Write-Host "`nFolders: $folders"
foreach ($f in $folders) { foreach ($f in $folders) {
$folderName = $f.Name $folderName = $f.Name
$createTime = Get-TimestampFromFolderName -Folder $f $createTime = Get-TimestampFromFolderName -Folder $f
@ -213,8 +209,8 @@ if (-not (Get-Module -ListAvailable -Name PnP.PowerShell)) {
} }
if ([string]::IsNullOrEmpty($ClientId) -or [string]::IsNullOrEmpty($AppCert)) { if ([string]::IsNullOrEmpty($AdminUser) -or [string]::IsNullOrEmpty($AdminPwd)) {
Write-Host "ClientId and AppCert required as arguments or environment variables." Write-Host "Admin user name and password required as arguments or environment variables."
Exit Exit
} }
@ -255,8 +251,12 @@ else {
Exit Exit
} }
$password = convertto-securestring -String $AdminPwd -AsPlainText -Force
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $AdminUser, $password
Write-Host "`nAuthenticating and connecting to $SiteUrl" Write-Host "`nAuthenticating and connecting to $SiteUrl"
Connect-PnPOnline -Url $siteUrl -ClientId $ClientId -CertificateBase64Encoded $AppCert -Tenant $TenantDomain Connect-PnPOnline -Url $siteUrl -Credential $cred
Write-Host "Connected to $siteUrl`n" Write-Host "Connected to $siteUrl`n"
# ensure that there are no unexpanded entries in the list of parameters # ensure that there are no unexpanded entries in the list of parameters

View File

@ -5,7 +5,6 @@ import (
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
"golang.org/x/exp/slices"
"github.com/alcionai/corso/src/cmd/sanity_test/common" "github.com/alcionai/corso/src/cmd/sanity_test/common"
"github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/ptr"
@ -21,20 +20,19 @@ const (
// this increases the chance that we'll run into a race collision with // this increases the chance that we'll run into a race collision with
// the cleanup script. Sometimes that's okay (deleting old data that // the cleanup script. Sometimes that's okay (deleting old data that
// isn't scrutinized in the test), other times it's not. We mark whether // isn't scrutinized in the test), other times it's not. We mark whether
// that's okay to do or not by specifying the folders being // that's okay to do or not by specifying the folder that's being
// scrutinized for the test. Any errors within those folders should cause // scrutinized for the test. Any errors within that folder should cause
// a fatal exit. Errors outside of those folders get ignored. // a fatal exit. Errors outside of that folder get ignored.
// //
// since we're using folder names, mustPopulateFolders will // since we're using folder names, requireNoErrorsWithinFolderName will
// work best (ie: have the fewest collisions/side-effects) if the folder // work best (ie: have the fewest collisions/side-effects) if the folder
// names are very specific. Standard sanity tests should include timestamps, // name is very specific. Standard sanity tests should include timestamps,
// which should help ensure that. Be warned if you try to use it with // which should help ensure that. Be warned if you try to use it with
// a more generic name: unintended effects could occur. // a more generic name: unintended effects could occur.
func populateSanitree( func populateSanitree(
ctx context.Context, ctx context.Context,
ac api.Client, ac api.Client,
driveID string, driveID, requireNoErrorsWithinFolderName string,
mustPopulateFolders []string,
) *common.Sanitree[models.DriveItemable, models.DriveItemable] { ) *common.Sanitree[models.DriveItemable, models.DriveItemable] {
common.Infof(ctx, "building sanitree for drive: %s", driveID) common.Infof(ctx, "building sanitree for drive: %s", driveID)
@ -58,8 +56,8 @@ func populateSanitree(
ac, ac,
driveID, driveID,
stree.Name+"/", stree.Name+"/",
mustPopulateFolders, requireNoErrorsWithinFolderName,
slices.Contains(mustPopulateFolders, rootName), rootName == requireNoErrorsWithinFolderName,
stree) stree)
return stree return stree
@ -68,9 +66,7 @@ func populateSanitree(
func recursivelyBuildTree( func recursivelyBuildTree(
ctx context.Context, ctx context.Context,
ac api.Client, ac api.Client,
driveID string, driveID, location, requireNoErrorsWithinFolderName string,
location string,
mustPopulateFolders []string,
isChildOfFolderRequiringNoErrors bool, isChildOfFolderRequiringNoErrors bool,
stree *common.Sanitree[models.DriveItemable, models.DriveItemable], stree *common.Sanitree[models.DriveItemable, models.DriveItemable],
) { ) {
@ -84,9 +80,9 @@ func recursivelyBuildTree(
common.Infof( common.Infof(
ctx, ctx,
"ignoring error getting children in directory %q because it is not within directory set %v\nerror: %s\n%+v", "ignoring error getting children in directory %q because it is not within directory %q\nerror: %s\n%+v",
location, location,
mustPopulateFolders, requireNoErrorsWithinFolderName,
err.Error(), err.Error(),
clues.ToCore(err)) clues.ToCore(err))
@ -103,12 +99,11 @@ func recursivelyBuildTree(
// currently we don't restore blank folders. // currently we don't restore blank folders.
// skip permission check for empty folders // skip permission check for empty folders
if ptr.Val(driveItem.GetFolder().GetChildCount()) == 0 { if ptr.Val(driveItem.GetFolder().GetChildCount()) == 0 {
common.Infof(ctx, "skipped empty folder: %s%s", location, itemName) common.Infof(ctx, "skipped empty folder: %s/%s", location, itemName)
continue continue
} }
cannotAllowErrors := isChildOfFolderRequiringNoErrors || cannotAllowErrors := isChildOfFolderRequiringNoErrors || itemName == requireNoErrorsWithinFolderName
slices.Contains(mustPopulateFolders, itemName)
branch := &common.Sanitree[models.DriveItemable, models.DriveItemable]{ branch := &common.Sanitree[models.DriveItemable, models.DriveItemable]{
Parent: stree, Parent: stree,
@ -129,7 +124,7 @@ func recursivelyBuildTree(
ac, ac,
driveID, driveID,
location+branch.Name+"/", location+branch.Name+"/",
mustPopulateFolders, requireNoErrorsWithinFolderName,
cannotAllowErrors, cannotAllowErrors,
branch) branch)
} }

View File

@ -32,7 +32,7 @@ func CheckExport(
ctx, ctx,
ac, ac,
driveID, driveID,
[]string{envs.SourceContainer}) envs.RestoreContainer)
sourceTree, ok := root.Children[envs.SourceContainer] sourceTree, ok := root.Children[envs.SourceContainer]
common.Assert( common.Assert(

View File

@ -45,14 +45,7 @@ func CheckRestoration(
"drive_id", driveID, "drive_id", driveID,
"drive_name", driveName) "drive_name", driveName)
root := populateSanitree( root := populateSanitree(ctx, ac, driveID, envs.RestoreContainer)
ctx,
ac,
driveID,
[]string{
envs.SourceContainer,
envs.RestoreContainer,
})
sourceTree, ok := root.Children[envs.SourceContainer] sourceTree, ok := root.Children[envs.SourceContainer]
common.Assert( common.Assert(

View File

@ -3,7 +3,7 @@ module github.com/alcionai/corso/src
go 1.21 go 1.21
replace ( replace (
github.com/kopia/kopia => github.com/alcionai/kopia v0.12.2-0.20240322180947-41471159a0a4 github.com/kopia/kopia => github.com/alcionai/kopia v0.12.2-0.20240116215733-ec3d100029fe
// Alcion fork removes the validation of email addresses as we might get incomplete email addresses // Alcion fork removes the validation of email addresses as we might get incomplete email addresses
github.com/xhit/go-simple-mail/v2 v2.16.0 => github.com/alcionai/go-simple-mail/v2 v2.0.0-20231220071811-c70ebcd9a41a github.com/xhit/go-simple-mail/v2 v2.16.0 => github.com/alcionai/go-simple-mail/v2 v2.0.0-20231220071811-c70ebcd9a41a
@ -121,7 +121,7 @@ require (
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/microsoft/kiota-serialization-text-go v1.0.0 github.com/microsoft/kiota-serialization-text-go v1.0.0
github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.67 github.com/minio/minio-go/v7 v7.0.66
github.com/minio/sha256-simd v1.0.1 // indirect github.com/minio/sha256-simd v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect

View File

@ -23,8 +23,8 @@ github.com/alcionai/clues v0.0.0-20240125221452-9fc7746dd20c h1:QtARFaqYKtGjmEej
github.com/alcionai/clues v0.0.0-20240125221452-9fc7746dd20c/go.mod h1:1YJwJy3W6GGsC2UiDAEWABUjgvT8OZHjKs8OoaXeKbw= github.com/alcionai/clues v0.0.0-20240125221452-9fc7746dd20c/go.mod h1:1YJwJy3W6GGsC2UiDAEWABUjgvT8OZHjKs8OoaXeKbw=
github.com/alcionai/go-simple-mail/v2 v2.0.0-20231220071811-c70ebcd9a41a h1:4nhM0NM1qtUT1s55rQ+D0Xw1Re5mIU9/crjEl6KdE+k= github.com/alcionai/go-simple-mail/v2 v2.0.0-20231220071811-c70ebcd9a41a h1:4nhM0NM1qtUT1s55rQ+D0Xw1Re5mIU9/crjEl6KdE+k=
github.com/alcionai/go-simple-mail/v2 v2.0.0-20231220071811-c70ebcd9a41a/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98= github.com/alcionai/go-simple-mail/v2 v2.0.0-20231220071811-c70ebcd9a41a/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
github.com/alcionai/kopia v0.12.2-0.20240322180947-41471159a0a4 h1:3YZ70H3mkUgwiHLiNvukrqh2awRgfl1RAkbV0IoUqqk= github.com/alcionai/kopia v0.12.2-0.20240116215733-ec3d100029fe h1:nLS5pxhm04Jz4+qeipNlxdyPGxqNWpBu8UGkRYpWoIw=
github.com/alcionai/kopia v0.12.2-0.20240322180947-41471159a0a4/go.mod h1:QFRSOUQzZfKE3hKVBHP7hxOn5WyrEmdBtfN5wkib/eA= github.com/alcionai/kopia v0.12.2-0.20240116215733-ec3d100029fe/go.mod h1:QFRSOUQzZfKE3hKVBHP7hxOn5WyrEmdBtfN5wkib/eA=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@ -219,8 +219,8 @@ github.com/microsoftgraph/msgraph-sdk-go-core v1.0.1 h1:uq4qZD8VXLiNZY0t4NoRpLDo
github.com/microsoftgraph/msgraph-sdk-go-core v1.0.1/go.mod h1:HUITyuFN556+0QZ/IVfH5K4FyJM7kllV6ExKi2ImKhE= github.com/microsoftgraph/msgraph-sdk-go-core v1.0.1/go.mod h1:HUITyuFN556+0QZ/IVfH5K4FyJM7kllV6ExKi2ImKhE=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.67 h1:BeBvZWAS+kRJm1vGTMJYVjKUNoo0FoEt/wUWdUtfmh8= github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw=
github.com/minio/minio-go/v7 v7.0.67/go.mod h1:+UXocnUeZ3wHvVh5s95gcrA4YjMIbccT6ubB+1m054A= github.com/minio/minio-go/v7 v7.0.66/go.mod h1:DHAgmyQEGdW3Cif0UooKOyrT3Vxs82zNdV6tkKhRtbs=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=

View File

@ -10,7 +10,6 @@ import (
"github.com/alcionai/corso/src/pkg/dttm" "github.com/alcionai/corso/src/pkg/dttm"
"github.com/alcionai/corso/src/pkg/export" "github.com/alcionai/corso/src/pkg/export"
"github.com/alcionai/corso/src/pkg/logger"
) )
const ( const (
@ -57,22 +56,12 @@ func ZipExportCollection(
defer wr.Close() defer wr.Close()
buf := make([]byte, ZipCopyBufferSize) buf := make([]byte, ZipCopyBufferSize)
counted := 0
log := logger.Ctx(ctx).
With("collection_count", len(expCollections))
for _, ec := range expCollections { for _, ec := range expCollections {
folder := ec.BasePath() folder := ec.BasePath()
items := ec.Items(ctx) items := ec.Items(ctx)
for item := range items { for item := range items {
counted++
// Log every 1000 items that are processed
if counted%1000 == 0 {
log.Infow("progress zipping export items", "count_items", counted)
}
err := item.Error err := item.Error
if err != nil { if err != nil {
writer.CloseWithError(clues.Wrap(err, "getting export item").With("id", item.ID)) writer.CloseWithError(clues.Wrap(err, "getting export item").With("id", item.ID))
@ -99,12 +88,8 @@ func ZipExportCollection(
writer.CloseWithError(clues.Wrap(err, "writing zip entry").With("name", name).With("id", item.ID)) writer.CloseWithError(clues.Wrap(err, "writing zip entry").With("name", name).With("id", item.ID))
return return
} }
item.Body.Close()
} }
} }
log.Infow("completed zipping export items", "count_items", counted)
}() }()
return zipCollection{reader}, nil return zipCollection{reader}, nil

View File

@ -1,13 +1,10 @@
package jwt package jwt
import ( import (
"context"
"time" "time"
"github.com/alcionai/clues" "github.com/alcionai/clues"
jwt "github.com/golang-jwt/jwt/v5" jwt "github.com/golang-jwt/jwt/v5"
"github.com/alcionai/corso/src/pkg/logger"
) )
// IsJWTExpired checks if the JWT token is past expiry by analyzing the // IsJWTExpired checks if the JWT token is past expiry by analyzing the
@ -40,51 +37,3 @@ func IsJWTExpired(
return expired, nil return expired, nil
} }
// GetJWTLifetime returns the issued at(iat) and expiration time(exp) claims
// present in the JWT token. These are optional claims and may not be present
// in the token. Absence is not reported as an error.
//
// An error is returned if the supplied token is malformed. Times are returned
// in UTC to have parity with graph responses.
func GetJWTLifetime(
ctx context.Context,
rawToken string,
) (time.Time, time.Time, error) {
var (
issuedAt time.Time
expiresAt time.Time
)
p := jwt.NewParser()
token, _, err := p.ParseUnverified(rawToken, &jwt.RegisteredClaims{})
if err != nil {
logger.CtxErr(ctx, err).Debug("parsing jwt token")
return time.Time{}, time.Time{}, clues.Wrap(err, "invalid jwt")
}
exp, err := token.Claims.GetExpirationTime()
if err != nil {
logger.CtxErr(ctx, err).Debug("extracting exp claim")
return time.Time{}, time.Time{}, clues.Wrap(err, "getting token expiry time")
}
iat, err := token.Claims.GetIssuedAt()
if err != nil {
logger.CtxErr(ctx, err).Debug("extracting iat claim")
return time.Time{}, time.Time{}, clues.Wrap(err, "getting token issued at time")
}
// Absence of iat or exp claims is not reported as an error by jwt library as these
// are optional as per spec.
if iat != nil {
issuedAt = iat.UTC()
}
if exp != nil {
expiresAt = exp.UTC()
}
return issuedAt, expiresAt, nil
}

View File

@ -113,134 +113,3 @@ func (suite *JWTUnitSuite) TestIsJWTExpired() {
}) })
} }
} }
func (suite *JWTUnitSuite) TestGetJWTLifetime() {
// Set of time values to be used in the tests.
// Truncate to seconds for comparisons since jwt tokens have second
// level precision.
idToTime := map[string]time.Time{
"T0": time.Now().UTC().Add(-time.Hour).Truncate(time.Second),
"T1": time.Now().UTC().Truncate(time.Second),
"T2": time.Now().UTC().Add(time.Hour).Truncate(time.Second),
}
table := []struct {
name string
getToken func() (string, error)
expectFunc func(t *testing.T, iat time.Time, exp time.Time)
expectErr assert.ErrorAssertionFunc
}{
{
name: "alive token",
getToken: func() (string, error) {
return createJWTToken(
jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(idToTime["T0"]),
ExpiresAt: jwt.NewNumericDate(idToTime["T1"]),
})
},
expectFunc: func(t *testing.T, iat time.Time, exp time.Time) {
assert.Equal(t, idToTime["T0"], iat)
assert.Equal(t, idToTime["T1"], exp)
},
expectErr: assert.NoError,
},
// Test with a token which is not generated using the go-jwt lib.
// This is a long lived token which is valid for 100 years.
{
name: "alive raw token with iat and exp claims",
getToken: func() (string, error) {
return rawToken, nil
},
expectFunc: func(t *testing.T, iat time.Time, exp time.Time) {
assert.Less(t, iat, time.Now(), "iat should be in the past")
assert.Greater(t, exp, time.Now(), "exp should be in the future")
},
expectErr: assert.NoError,
},
// Regardless of whether the token is expired or not, we should be able to
// extract the iat and exp claims from it without error.
{
name: "expired token",
getToken: func() (string, error) {
return createJWTToken(
jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(idToTime["T1"]),
ExpiresAt: jwt.NewNumericDate(idToTime["T0"]),
})
},
expectFunc: func(t *testing.T, iat time.Time, exp time.Time) {
assert.Equal(t, idToTime["T1"], iat)
assert.Equal(t, idToTime["T0"], exp)
},
expectErr: assert.NoError,
},
{
name: "missing iat claim",
getToken: func() (string, error) {
return createJWTToken(
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(idToTime["T2"]),
})
},
expectFunc: func(t *testing.T, iat time.Time, exp time.Time) {
assert.Equal(t, time.Time{}, iat)
assert.Equal(t, idToTime["T2"], exp)
},
expectErr: assert.NoError,
},
{
name: "missing exp claim",
getToken: func() (string, error) {
return createJWTToken(
jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(idToTime["T0"]),
})
},
expectFunc: func(t *testing.T, iat time.Time, exp time.Time) {
assert.Equal(t, idToTime["T0"], iat)
assert.Equal(t, time.Time{}, exp)
},
expectErr: assert.NoError,
},
{
name: "both claims missing",
getToken: func() (string, error) {
return createJWTToken(jwt.RegisteredClaims{})
},
expectFunc: func(t *testing.T, iat time.Time, exp time.Time) {
assert.Equal(t, time.Time{}, iat)
assert.Equal(t, time.Time{}, exp)
},
expectErr: assert.NoError,
},
{
name: "malformed token",
getToken: func() (string, error) {
return "header.claims.signature", nil
},
expectFunc: func(t *testing.T, iat time.Time, exp time.Time) {
assert.Equal(t, time.Time{}, iat)
assert.Equal(t, time.Time{}, exp)
},
expectErr: assert.Error,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
token, err := test.getToken()
require.NoError(t, err)
iat, exp, err := GetJWTLifetime(ctx, token)
test.expectErr(t, err)
test.expectFunc(t, iat, exp)
})
}
}

View File

@ -59,19 +59,6 @@ func First(vs ...string) string {
return "" return ""
} }
// FirstIn returns the first entry in the map with a non-zero value
// when iterating the provided list of keys.
func FirstIn(m map[string]any, keys ...string) string {
for _, key := range keys {
v, err := AnyValueToString(key, m)
if err == nil && len(v) > 0 {
return v
}
}
return ""
}
// Preview reduces the string to the specified size. // Preview reduces the string to the specified size.
// If the string is longer than the size, the last three // If the string is longer than the size, the last three
// characters are replaced with an ellipsis. Size < 4 // characters are replaced with an ellipsis. Size < 4

View File

@ -118,96 +118,3 @@ func TestGenerateHash(t *testing.T) {
} }
} }
} }
func TestFirstIn(t *testing.T) {
table := []struct {
name string
m map[string]any
keys []string
expect string
}{
{
name: "nil map",
keys: []string{"foo", "bar"},
expect: "",
},
{
name: "empty map",
m: map[string]any{},
keys: []string{"foo", "bar"},
expect: "",
},
{
name: "no match",
m: map[string]any{
"baz": "baz",
},
keys: []string{"foo", "bar"},
expect: "",
},
{
name: "no keys",
m: map[string]any{
"baz": "baz",
},
keys: []string{},
expect: "",
},
{
name: "nil match",
m: map[string]any{
"foo": nil,
},
keys: []string{"foo", "bar"},
expect: "",
},
{
name: "empty match",
m: map[string]any{
"foo": "",
},
keys: []string{"foo", "bar"},
expect: "",
},
{
name: "matches first key",
m: map[string]any{
"foo": "fnords",
},
keys: []string{"foo", "bar"},
expect: "fnords",
},
{
name: "matches second key",
m: map[string]any{
"bar": "smarf",
},
keys: []string{"foo", "bar"},
expect: "smarf",
},
{
name: "matches second key with nil first match",
m: map[string]any{
"foo": nil,
"bar": "smarf",
},
keys: []string{"foo", "bar"},
expect: "smarf",
},
{
name: "matches second key with empty first match",
m: map[string]any{
"foo": "",
"bar": "smarf",
},
keys: []string{"foo", "bar"},
expect: "smarf",
},
}
for _, test := range table {
t.Run(test.name, func(t *testing.T) {
result := FirstIn(test.m, test.keys...)
assert.Equal(t, test.expect, result)
})
}
}

View File

@ -143,121 +143,6 @@ func getICalData(ctx context.Context, data models.Messageable) (string, error) {
return ics.FromEventable(ctx, event) return ics.FromEventable(ctx, event)
} }
func getFileAttachment(ctx context.Context, attachment models.Attachmentable) (*mail.File, error) {
kind := ptr.Val(attachment.GetContentType())
bytes, err := attachment.GetBackingStore().Get("contentBytes")
if err != nil {
return nil, clues.WrapWC(ctx, err, "failed to get attachment bytes").
With("kind", kind)
}
if bytes == nil {
// TODO(meain): Handle non file attachments
// https://github.com/alcionai/corso/issues/4772
logger.Ctx(ctx).
With("attachment_id", ptr.Val(attachment.GetId()),
"attachment_type", ptr.Val(attachment.GetOdataType())).
Info("no contentBytes for attachment")
return nil, nil
}
bts, ok := bytes.([]byte)
if !ok {
return nil, clues.WrapWC(ctx, err, "invalid content bytes").
With("kind", kind).
With("interface_type", fmt.Sprintf("%T", bytes))
}
name := ptr.Val(attachment.GetName())
if len(name) == 0 {
// Graph as of now does not let us create any attachments
// without a name, but we have run into instances where we have
// see attachments without a name, possibly from old
// data. This is for those cases.
name = "Unnamed"
}
contentID, err := attachment.GetBackingStore().Get("contentId")
if err != nil {
return nil, clues.WrapWC(ctx, err, "getting content id for attachment").
With("kind", kind)
}
if contentID != nil {
cids, _ := str.AnyToString(contentID)
if len(cids) > 0 {
name = cids
}
}
return &mail.File{
// cannot use filename as inline attachment will not get mapped properly
Name: name,
MimeType: kind,
Data: bts,
Inline: ptr.Val(attachment.GetIsInline()),
}, nil
}
func getItemAttachment(ctx context.Context, attachment models.Attachmentable) (*mail.File, error) {
it, err := attachment.GetBackingStore().Get("item")
if err != nil {
return nil, clues.WrapWC(ctx, err, "getting item for attachment").
With("attachment_id", ptr.Val(attachment.GetId()))
}
name := ptr.Val(attachment.GetName())
if len(name) == 0 {
// Graph as of now does not let us create any attachments
// without a name, but we have run into instances where we have
// see attachments without a name, possibly from old
// data. This is for those cases.
name = "Unnamed"
}
switch it := it.(type) {
case *models.Message:
cb, err := FromMessageable(ctx, it)
if err != nil {
return nil, clues.WrapWC(ctx, err, "converting item attachment to eml").
With("attachment_id", ptr.Val(attachment.GetId()))
}
return &mail.File{
Name: name,
MimeType: "message/rfc822",
Data: []byte(cb),
}, nil
default:
logger.Ctx(ctx).
With("attachment_id", ptr.Val(attachment.GetId()),
"attachment_type", ptr.Val(attachment.GetOdataType())).
Info("unknown item attachment type")
}
return nil, nil
}
func getMailAttachment(ctx context.Context, att models.Attachmentable) (*mail.File, error) {
otyp := ptr.Val(att.GetOdataType())
switch otyp {
case "#microsoft.graph.fileAttachment":
return getFileAttachment(ctx, att)
case "#microsoft.graph.itemAttachment":
return getItemAttachment(ctx, att)
default:
logger.Ctx(ctx).
With("attachment_id", ptr.Val(att.GetId()),
"attachment_type", otyp).
Info("unknown attachment type")
return nil, nil
}
}
// FromJSON converts a Messageable (as json) to .eml format // FromJSON converts a Messageable (as json) to .eml format
func FromJSON(ctx context.Context, body []byte) (string, error) { func FromJSON(ctx context.Context, body []byte) (string, error) {
ctx = clues.Add(ctx, "body_len", len(body)) ctx = clues.Add(ctx, "body_len", len(body))
@ -267,11 +152,6 @@ func FromJSON(ctx context.Context, body []byte) (string, error) {
return "", clues.WrapWC(ctx, err, "converting to messageble") return "", clues.WrapWC(ctx, err, "converting to messageble")
} }
return FromMessageable(ctx, data)
}
// Converts a Messageable to .eml format
func FromMessageable(ctx context.Context, data models.Messageable) (string, error) {
ctx = clues.Add(ctx, "item_id", ptr.Val(data.GetId())) ctx = clues.Add(ctx, "item_id", ptr.Val(data.GetId()))
email := mail.NewMSG() email := mail.NewMSG()
@ -349,16 +229,54 @@ func FromMessageable(ctx context.Context, data models.Messageable) (string, erro
if data.GetAttachments() != nil { if data.GetAttachments() != nil {
for _, attachment := range data.GetAttachments() { for _, attachment := range data.GetAttachments() {
att, err := getMailAttachment(ctx, attachment) kind := ptr.Val(attachment.GetContentType())
bytes, err := attachment.GetBackingStore().Get("contentBytes")
if err != nil { if err != nil {
return "", clues.WrapWC(ctx, err, "getting mail attachment") return "", clues.WrapWC(ctx, err, "failed to get attachment bytes").
With("kind", kind)
} }
// There are known cases where we just wanna log and if bytes == nil {
// ignore instead of erroring out // TODO(meain): Handle non file attachments
if att != nil { // https://github.com/alcionai/corso/issues/4772
email.Attach(att) logger.Ctx(ctx).
With("attachment_id", ptr.Val(attachment.GetId()),
"attachment_type", ptr.Val(attachment.GetOdataType())).
Info("no contentBytes for attachment")
continue
} }
bts, ok := bytes.([]byte)
if !ok {
return "", clues.WrapWC(ctx, err, "invalid content bytes").
With("kind", kind).
With("interface_type", fmt.Sprintf("%T", bytes))
}
name := ptr.Val(attachment.GetName())
contentID, err := attachment.GetBackingStore().Get("contentId")
if err != nil {
return "", clues.WrapWC(ctx, err, "getting content id for attachment").
With("kind", kind)
}
if contentID != nil {
cids, _ := str.AnyToString(contentID)
if len(cids) > 0 {
name = cids
}
}
email.Attach(&mail.File{
// cannot use filename as inline attachment will not get mapped properly
Name: name,
MimeType: kind,
Data: bts,
Inline: ptr.Val(attachment.GetIsInline()),
})
} }
} }
@ -380,7 +298,7 @@ func FromMessageable(ctx context.Context, data models.Messageable) (string, erro
} }
} }
if err := email.GetError(); err != nil { if err = email.GetError(); err != nil {
return "", clues.WrapWC(ctx, err, "converting to eml") return "", clues.WrapWC(ctx, err, "converting to eml")
} }
@ -488,9 +406,6 @@ func FromJSONPostToEML(
} }
name := ptr.Val(attachment.GetName()) name := ptr.Val(attachment.GetName())
if len(name) == 0 {
name = "Unnamed"
}
contentID, err := attachment.GetBackingStore().Get("contentId") contentID, err := attachment.GetBackingStore().Get("contentId")
if err != nil { if err != nil {

View File

@ -137,11 +137,6 @@ func (suite *EMLUnitSuite) TestConvert_messageble_to_eml() {
} }
func (suite *EMLUnitSuite) TestConvert_edge_cases() { func (suite *EMLUnitSuite) TestConvert_edge_cases() {
bodies := []string{
testdata.EmailWithAttachments,
testdata.EmailWithinEmail,
}
tests := []struct { tests := []struct {
name string name string
transform func(models.Messageable) transform func(models.Messageable)
@ -167,47 +162,8 @@ func (suite *EMLUnitSuite) TestConvert_edge_cases() {
require.NoError(suite.T(), err, "setting attachment content") require.NoError(suite.T(), err, "setting attachment content")
}, },
}, },
{
name: "attachment without name",
transform: func(msg models.Messageable) {
attachments := msg.GetAttachments()
attachments[1].SetName(ptr.To(""))
// This test has to be run on a non inline attachment
// as inline attachments use contentID instead of name
// even when there is a name.
assert.False(suite.T(), ptr.Val(attachments[1].GetIsInline()))
},
},
{
name: "attachment with nil name",
transform: func(msg models.Messageable) {
attachments := msg.GetAttachments()
attachments[1].SetName(nil)
// This test has to be run on a non inline attachment
// as inline attachments use contentID instead of name
// even when there is a name.
assert.False(suite.T(), ptr.Val(attachments[1].GetIsInline()))
},
},
{
name: "multiple attachments without name",
transform: func(msg models.Messageable) {
attachments := msg.GetAttachments()
attachments[1].SetName(ptr.To(""))
attachments[2].SetName(ptr.To(""))
// This test has to be run on a non inline attachment
// as inline attachments use contentID instead of name
// even when there is a name.
assert.False(suite.T(), ptr.Val(attachments[1].GetIsInline()))
assert.False(suite.T(), ptr.Val(attachments[2].GetIsInline()))
},
},
} }
for _, b := range bodies {
for _, test := range tests { for _, test := range tests {
suite.Run(test.name, func() { suite.Run(test.name, func() {
t := suite.T() t := suite.T()
@ -215,7 +171,7 @@ func (suite *EMLUnitSuite) TestConvert_edge_cases() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
body := []byte(b) body := []byte(testdata.EmailWithAttachments)
msg, err := api.BytesToMessageable(body) msg, err := api.BytesToMessageable(body)
require.NoError(t, err, "creating message") require.NoError(t, err, "creating message")
@ -237,7 +193,6 @@ func (suite *EMLUnitSuite) TestConvert_edge_cases() {
}) })
} }
} }
}
func (suite *EMLUnitSuite) TestConvert_eml_ics() { func (suite *EMLUnitSuite) TestConvert_eml_ics() {
t := suite.T() t := suite.T()
@ -273,11 +228,11 @@ func (suite *EMLUnitSuite) TestConvert_eml_ics() {
assert.Equal( assert.Equal(
t, t,
msg.GetCreatedDateTime().Format(ics.ICalDateTimeFormatUTC), msg.GetCreatedDateTime().Format(ics.ICalDateTimeFormat),
event.GetProperty(ical.ComponentPropertyCreated).Value) event.GetProperty(ical.ComponentPropertyCreated).Value)
assert.Equal( assert.Equal(
t, t,
msg.GetLastModifiedDateTime().Format(ics.ICalDateTimeFormatUTC), msg.GetLastModifiedDateTime().Format(ics.ICalDateTimeFormat),
event.GetProperty(ical.ComponentPropertyLastModified).Value) event.GetProperty(ical.ComponentPropertyLastModified).Value)
st, err := ics.GetUTCTime( st, err := ics.GetUTCTime(
@ -292,11 +247,11 @@ func (suite *EMLUnitSuite) TestConvert_eml_ics() {
assert.Equal( assert.Equal(
t, t,
st.Format(ics.ICalDateTimeFormatUTC), st.Format(ics.ICalDateTimeFormat),
event.GetProperty(ical.ComponentPropertyDtStart).Value) event.GetProperty(ical.ComponentPropertyDtStart).Value)
assert.Equal( assert.Equal(
t, t,
et.Format(ics.ICalDateTimeFormatUTC), et.Format(ics.ICalDateTimeFormat),
event.GetProperty(ical.ComponentPropertyDtEnd).Value) event.GetProperty(ical.ComponentPropertyDtEnd).Value)
tos := msg.GetToRecipients() tos := msg.GetToRecipients()
@ -443,48 +398,3 @@ func (suite *EMLUnitSuite) TestConvert_postable_to_eml() {
assert.Equal(t, source, target) assert.Equal(t, source, target)
} }
// Tests an ics within an eml within another eml
func (suite *EMLUnitSuite) TestConvert_message_in_messageble_to_eml() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
body := []byte(testdata.EmailWithinEmail)
out, err := FromJSON(ctx, body)
assert.NoError(t, err, "converting to eml")
msg, err := api.BytesToMessageable(body)
require.NoError(t, err, "creating message")
eml, err := enmime.ReadEnvelope(strings.NewReader(out))
require.NoError(t, err, "reading created eml")
assert.Equal(t, ptr.Val(msg.GetSubject()), eml.GetHeader("Subject"))
assert.Equal(t, msg.GetSentDateTime().Format(time.RFC1123Z), eml.GetHeader("Date"))
assert.Equal(t, formatAddress(msg.GetFrom().GetEmailAddress()), eml.GetHeader("From"))
attachments := eml.Attachments
assert.Equal(t, 3, len(attachments), "attachment count in parent email")
ieml, err := enmime.ReadEnvelope(strings.NewReader(string(attachments[0].Content)))
require.NoError(t, err, "reading created eml")
itm, err := msg.GetAttachments()[0].GetBackingStore().Get("item")
require.NoError(t, err, "getting item from message")
imsg := itm.(*models.Message)
assert.Equal(t, ptr.Val(imsg.GetSubject()), ieml.GetHeader("Subject"))
assert.Equal(t, imsg.GetSentDateTime().Format(time.RFC1123Z), ieml.GetHeader("Date"))
assert.Equal(t, formatAddress(imsg.GetFrom().GetEmailAddress()), ieml.GetHeader("From"))
iattachments := ieml.Attachments
assert.Equal(t, 1, len(iattachments), "attachment count in child email")
// Known from testdata
assert.Contains(t, string(iattachments[0].Content), "X-LIC-LOCATION:Africa/Abidjan")
}

View File

@ -104,19 +104,6 @@
"contentId": null, "contentId": null,
"contentLocation": null, "contentLocation": null,
"contentBytes": "W1BhdGhzXQpQcmVmaXggPSAuLgo=" "contentBytes": "W1BhdGhzXQpQcmVmaXggPSAuLgo="
},
{
"@odata.type": "#microsoft.graph.fileAttachment",
"@odata.mediaContentType": "application/octet-stream",
"id": "ZZMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAEJAABBFDg0JJk7TY1fmsJrh7tNAAEwbDEWAAABEgAQAD3rU0iyzCdHgz0xmOrWc9g=",
"lastModifiedDateTime": "2023-11-16T05:42:47Z",
"name": "qt2.conf",
"contentType": "application/octet-stream",
"size": 156,
"isInline": false,
"contentId": null,
"contentLocation": null,
"contentBytes": "Z1BhdGhzXQpQcmVmaXggPSAuLgo="
} }
] ]
} }

View File

@ -1,268 +0,0 @@
{
"id": "AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAEJAABBFDg0JJk7TY1fmsJrh7tNAAFnbV-qAAA=",
"@odata.type": "#microsoft.graph.message",
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('7ceb8e03-bdc5-4509-a136-457526165ec0')/messages/$entity",
"@odata.etag": "W/\"CQAAABYAAABBFDg0JJk7TY1fmsJrh7tNAAFnDeBl\"",
"categories": [],
"changeKey": "CQAAABYAAABBFDg0JJk7TY1fmsJrh7tNAAFnDeBl",
"createdDateTime": "2024-02-05T09:33:23Z",
"lastModifiedDateTime": "2024-02-05T09:33:48Z",
"attachments": [
{
"id": "AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAEJAABBFDg0JJk7TY1fmsJrh7tNAAFnbV-qAAABEgAQAEUyH0VS3HJBgHDlZdWZl0k=",
"@odata.type": "#microsoft.graph.itemAttachment",
"item@odata.navigationLink": "https://graph.microsoft.com/v1.0/users('7ceb8e03-bdc5-4509-a136-457526165ec0')/messages('')",
"item@odata.associationLink": "https://graph.microsoft.com/v1.0/users('7ceb8e03-bdc5-4509-a136-457526165ec0')/messages('')/$ref",
"isInline": false,
"lastModifiedDateTime": "2024-02-05T09:33:46Z",
"name": "Purpose of life",
"size": 11840,
"item": {
"id": "",
"@odata.type": "#microsoft.graph.message",
"createdDateTime": "2024-02-05T09:33:24Z",
"lastModifiedDateTime": "2024-02-05T09:33:46Z",
"attachments": [
{
"id": "AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAEJAABBFDg0JJk7TY1fmsJrh7tNAAFnbV-qAAACEgAQAEUyH0VS3HJBgHDlZdWZl0kSABAAjBhd4-oQaUS969pTkS-gzA==",
"@odata.type": "#microsoft.graph.fileAttachment",
"@odata.mediaContentType": "text/calendar",
"contentType": "text/calendar",
"isInline": false,
"lastModifiedDateTime": "2024-02-05T09:33:46Z",
"name": "Abidjan.ics",
"size": 573,
"contentBytes": "QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vdHp1cmwub3JnLy9OT05TR01MIE9sc29uIDIwMjNkLy9FTg0KVkVSU0lPTjoyLjANCkJFR0lOOlZUSU1FWk9ORQ0KVFpJRDpBZnJpY2EvQWJpZGphbg0KTEFTVC1NT0RJRklFRDoyMDIzMTIyMlQyMzMzNThaDQpUWlVSTDpodHRwczovL3d3dy50enVybC5vcmcvem9uZWluZm8vQWZyaWNhL0FiaWRqYW4NClgtTElDLUxPQ0FUSU9OOkFmcmljYS9BYmlkamFuDQpYLVBST0xFUFRJQy1UWk5BTUU6TE1UDQpCRUdJTjpTVEFOREFSRA0KVFpOQU1FOkdNVA0KVFpPRkZTRVRGUk9NOi0wMDE2MDgNClRaT0ZGU0VUVE86KzAwMDANCkRUU1RBUlQ6MTkxMjAxMDFUMDAwMDAwDQpFTkQ6U1RBTkRBUkQNCkVORDpWVElNRVpPTkUNCkVORDpWQ0FMRU5EQVINCg=="
}
],
"body": {
"content": "<html><head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"><style type=\"text/css\" style=\"display:none;\"> P {margin-top:0;margin-bottom:0;} </style></head><body dir=\"ltr\"><div class=\"elementToProof\" style=\"font-family: Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, Calibri, Helvetica, sans-serif; font-size: 12pt; color: rgb(0, 0, 0);\">I just realized the purpose of my life is to be a test case. Good to know.<br></div></body></html>",
"contentType": "html"
},
"bodyPreview": "I just realized the purpose of my life is to be a test case. Good to know.",
"conversationId": "AAQkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNAAQAFEnxDqYmbJEm8d2l3qfS6A=",
"conversationIndex": "AQHaWBYiUSfEOpiZskSbx3aXep9LoA==",
"flag": {
"flagStatus": "notFlagged"
},
"from": {
"emailAddress": {
"address": "JohannaL@10rqc2.onmicrosoft.com",
"name": "Johanna Lorenz"
}
},
"hasAttachments": true,
"importance": "normal",
"internetMessageId": "<SJ0PR04MB7294108E381BCCE5C207B6DEBC472@SJ0PR04MB7294.namprd04.prod.outlook.com>",
"isDeliveryReceiptRequested": false,
"isDraft": false,
"isRead": true,
"isReadReceiptRequested": false,
"receivedDateTime": "2024-02-05T09:33:12Z",
"sender": {
"emailAddress": {
"address": "JohannaL@10rqc2.onmicrosoft.com",
"name": "Johanna Lorenz"
}
},
"sentDateTime": "2024-02-05T09:33:11Z",
"subject": "Purpose of life",
"toRecipients": [
{
"emailAddress": {
"address": "PradeepG@10rqc2.onmicrosoft.com",
"name": "Pradeep Gupta"
}
}
],
"webLink": "https://outlook.office365.com/owa/?AttachmentItemID=AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAEJAABBFDg0JJk7TY1fmsJrh7tNAAFnbV%2FqAAABEgAQAEUyH0VS3HJBgHDlZdWZl0k%3D&exvsurl=1&viewmodel=ItemAttachment"
}
},
{
"id": "AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAEJAABBFDg0JJk7TY1fmsJrh7tNAAFnbV-qAAABEgAQAEUyH0VS3HJBgHDlZdWZl02=",
"@odata.type": "#microsoft.graph.itemAttachment",
"item@odata.navigationLink": "https://graph.microsoft.com/v1.0/users('7ceb8e03-bdc5-4509-a136-457526165ec0')/messages('')",
"item@odata.associationLink": "https://graph.microsoft.com/v1.0/users('7ceb8e03-bdc5-4509-a136-457526165ec0')/messages('')/$ref",
"isInline": false,
"lastModifiedDateTime": "2024-02-05T09:33:46Z",
"name": "Purpose of life part 2",
"size": 11840,
"item": {
"id": "",
"@odata.type": "#microsoft.graph.message",
"createdDateTime": "2024-02-05T09:33:24Z",
"lastModifiedDateTime": "2024-02-05T09:33:46Z",
"attachments": [
{
"id": "AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAEJAABBFDg0JJk7TY1fmsJrh7tNAAFnbV-qAAACEgAQAEUyH0VS3HJBgHDlZdWZl0kSABAAjBhd4-oQaUS969pTkS-gzA==",
"@odata.type": "#microsoft.graph.fileAttachment",
"@odata.mediaContentType": "text/calendar",
"contentType": "text/calendar",
"isInline": false,
"lastModifiedDateTime": "2024-02-05T09:33:46Z",
"name": "Abidjan.ics",
"size": 573,
"contentBytes": "QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vdHp1cmwub3JnLy9OT05TR01MIE9sc29uIDIwMjNkLy9FTg0KVkVSU0lPTjoyLjANCkJFR0lOOlZUSU1FWk9ORQ0KVFpJRDpBZnJpY2EvQWJpZGphbg0KTEFTVC1NT0RJRklFRDoyMDIzMTIyMlQyMzMzNThaDQpUWlVSTDpodHRwczovL3d3dy50enVybC5vcmcvem9uZWluZm8vQWZyaWNhL0FiaWRqYW4NClgtTElDLUxPQ0FUSU9OOkFmcmljYS9BYmlkamFuDQpYLVBST0xFUFRJQy1UWk5BTUU6TE1UDQpCRUdJTjpTVEFOREFSRA0KVFpOQU1FOkdNVA0KVFpPRkZTRVRGUk9NOi0wMDE2MDgNClRaT0ZGU0VUVE86KzAwMDANCkRUU1RBUlQ6MTkxMjAxMDFUMDAwMDAwDQpFTkQ6U1RBTkRBUkQNCkVORDpWVElNRVpPTkUNCkVORDpWQ0FMRU5EQVINCg=="
}
],
"body": {
"content": "<html><head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"><style type=\"text/css\" style=\"display:none;\"> P {margin-top:0;margin-bottom:0;} </style></head><body dir=\"ltr\"><div class=\"elementToProof\" style=\"font-family: Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, Calibri, Helvetica, sans-serif; font-size: 12pt; color: rgb(0, 0, 0);\">I just realized the purpose of my life is to be a test case. Good to know.<br></div></body></html>",
"contentType": "html"
},
"bodyPreview": "I just realized the purpose of my life is to be a test case. Good to know.",
"conversationId": "AAQkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNAAQAFEnxDqYmbJEm8d2l3qfS6A=",
"conversationIndex": "AQHaWBYiUSfEOpiZskSbx3aXep9LoA==",
"flag": {
"flagStatus": "notFlagged"
},
"from": {
"emailAddress": {
"address": "JohannaL@10rqc2.onmicrosoft.com",
"name": "Johanna Lorenz"
}
},
"hasAttachments": true,
"importance": "normal",
"internetMessageId": "<SJ0PR04MB7294108E381BCCE5C207B6DEBC472@SJ0PR04MB7294.namprd04.prod.outlook.com>",
"isDeliveryReceiptRequested": false,
"isDraft": false,
"isRead": true,
"isReadReceiptRequested": false,
"receivedDateTime": "2024-02-05T09:33:12Z",
"sender": {
"emailAddress": {
"address": "JohannaL@10rqc2.onmicrosoft.com",
"name": "Johanna Lorenz"
}
},
"sentDateTime": "2024-02-05T09:33:11Z",
"subject": "Purpose of life",
"toRecipients": [
{
"emailAddress": {
"address": "PradeepG@10rqc2.onmicrosoft.com",
"name": "Pradeep Gupta"
}
}
],
"webLink": "https://outlook.office365.com/owa/?AttachmentItemID=AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAEJAABBFDg0JJk7TY1fmsJrh7tNAAFnbV%2FqAAABEgAQAEUyH0VS3HJBgHDlZdWZl02%3D&exvsurl=1&viewmodel=ItemAttachment"
}
},
{
"id": "AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAEJAABBFDg0JJk7TY1fmsJrh7tNAAFnbV-qAAABEgAQAEUyH0VS3HJBgHDlZdWZl03=",
"@odata.type": "#microsoft.graph.itemAttachment",
"item@odata.navigationLink": "https://graph.microsoft.com/v1.0/users('7ceb8e03-bdc5-4509-a136-457526165ec0')/messages('')",
"item@odata.associationLink": "https://graph.microsoft.com/v1.0/users('7ceb8e03-bdc5-4509-a136-457526165ec0')/messages('')/$ref",
"isInline": false,
"lastModifiedDateTime": "2024-02-05T09:33:46Z",
"name": "Purpose of life part 3",
"size": 11840,
"item": {
"id": "",
"@odata.type": "#microsoft.graph.message",
"createdDateTime": "2024-02-05T09:33:24Z",
"lastModifiedDateTime": "2024-02-05T09:33:46Z",
"attachments": [
{
"id": "AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAEJAABBFDg0JJk7TY1fmsJrh7tNAAFnbV-qAAACEgAQAEUyH0VS3HJBgHDlZdWZl0kSABAAjBhd4-oQaUS969pTkS-gzA==",
"@odata.type": "#microsoft.graph.fileAttachment",
"@odata.mediaContentType": "text/calendar",
"contentType": "text/calendar",
"isInline": false,
"lastModifiedDateTime": "2024-02-05T09:33:46Z",
"name": "Abidjan.ics",
"size": 573,
"contentBytes": "QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vdHp1cmwub3JnLy9OT05TR01MIE9sc29uIDIwMjNkLy9FTg0KVkVSU0lPTjoyLjANCkJFR0lOOlZUSU1FWk9ORQ0KVFpJRDpBZnJpY2EvQWJpZGphbg0KTEFTVC1NT0RJRklFRDoyMDIzMTIyMlQyMzMzNThaDQpUWlVSTDpodHRwczovL3d3dy50enVybC5vcmcvem9uZWluZm8vQWZyaWNhL0FiaWRqYW4NClgtTElDLUxPQ0FUSU9OOkFmcmljYS9BYmlkamFuDQpYLVBST0xFUFRJQy1UWk5BTUU6TE1UDQpCRUdJTjpTVEFOREFSRA0KVFpOQU1FOkdNVA0KVFpPRkZTRVRGUk9NOi0wMDE2MDgNClRaT0ZGU0VUVE86KzAwMDANCkRUU1RBUlQ6MTkxMjAxMDFUMDAwMDAwDQpFTkQ6U1RBTkRBUkQNCkVORDpWVElNRVpPTkUNCkVORDpWQ0FMRU5EQVINCg=="
}
],
"body": {
"content": "<html><head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"><style type=\"text/css\" style=\"display:none;\"> P {margin-top:0;margin-bottom:0;} </style></head><body dir=\"ltr\"><div class=\"elementToProof\" style=\"font-family: Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, Calibri, Helvetica, sans-serif; font-size: 12pt; color: rgb(0, 0, 0);\">I just realized the purpose of my life is to be a test case. Good to know.<br></div></body></html>",
"contentType": "html"
},
"bodyPreview": "I just realized the purpose of my life is to be a test case. Good to know.",
"conversationId": "AAQkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNAAQAFEnxDqYmbJEm8d2l3qfS6A=",
"conversationIndex": "AQHaWBYiUSfEOpiZskSbx3aXep9LoA==",
"flag": {
"flagStatus": "notFlagged"
},
"from": {
"emailAddress": {
"address": "JohannaL@10rqc2.onmicrosoft.com",
"name": "Johanna Lorenz"
}
},
"hasAttachments": true,
"importance": "normal",
"internetMessageId": "<SJ0PR04MB7294108E381BCCE5C207B6DEBC472@SJ0PR04MB7294.namprd04.prod.outlook.com>",
"isDeliveryReceiptRequested": false,
"isDraft": false,
"isRead": true,
"isReadReceiptRequested": false,
"receivedDateTime": "2024-02-05T09:33:12Z",
"sender": {
"emailAddress": {
"address": "JohannaL@10rqc2.onmicrosoft.com",
"name": "Johanna Lorenz"
}
},
"sentDateTime": "2024-02-05T09:33:11Z",
"subject": "Purpose of life",
"toRecipients": [
{
"emailAddress": {
"address": "PradeepG@10rqc2.onmicrosoft.com",
"name": "Pradeep Gupta"
}
}
],
"webLink": "https://outlook.office365.com/owa/?AttachmentItemID=AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAEJAABBFDg0JJk7TY1fmsJrh7tNAAFnbV%2FqAAABEgAQAEUyH0VS3HJBgHDlZdWZl03%3D&exvsurl=1&viewmodel=ItemAttachment"
}
}
],
"bccRecipients": [],
"body": {
"content": "<html><head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"><style type=\"text/css\" style=\"display:none\">\r\n<!--\r\np\r\n\t{margin-top:0;\r\n\tmargin-bottom:0}\r\n-->\r\n</style></head><body dir=\"ltr\"><div><span class=\"elementToProof\" style=\"font-family:Aptos,Aptos_EmbeddedFont,Aptos_MSFontService,Calibri,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)\">Now, this is what we call nesting in this business.<br></span></div></body></html>",
"contentType": "html"
},
"bodyPreview": "Now, this is what we call nesting in this business.",
"ccRecipients": [],
"conversationId": "AAQkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNAAQAIv2-4RHwDhJhlqBV5PTE3Y=",
"conversationIndex": "AQHaWBZdi/b/hEfAOEmGWoFXk9MTdg==",
"flag": {
"flagStatus": "notFlagged"
},
"from": {
"emailAddress": {
"address": "JohannaL@10rqc2.onmicrosoft.com",
"name": "Johanna Lorenz"
}
},
"hasAttachments": true,
"importance": "normal",
"inferenceClassification": "focused",
"internetMessageId": "<SJ0PR04MB729409CE8C191E01151C110DBC472@SJ0PR04MB7294.namprd04.prod.outlook.com>",
"isDeliveryReceiptRequested": false,
"isDraft": false,
"isRead": true,
"isReadReceiptRequested": false,
"parentFolderId": "AQMkAGJiAGZhNjRlOC00OGI5LTQyNTItYjFkMy00NTJjMTgyZGZkMjQALgAAA0V2IruiJ9ZFvgAO6qBJFycBAEEUODQkmTtNjV_awmuHu00AAAIBCQAAAA==",
"receivedDateTime": "2024-02-05T09:33:46Z",
"replyTo": [],
"sender": {
"emailAddress": {
"address": "JohannaL@10rqc2.onmicrosoft.com",
"name": "Johanna Lorenz"
}
},
"sentDateTime": "2024-02-05T09:33:45Z",
"subject": "Fw: Purpose of life",
"toRecipients": [
{
"emailAddress": {
"address": "PradeepG@10rqc2.onmicrosoft.com",
"name": "Pradeep Gupta"
}
}
],
"webLink": "https://outlook.office365.com/owa/?ItemID=AAMkAGJiZmE2NGU4LTQ4YjktNDI1Mi1iMWQzLTQ1MmMxODJkZmQyNABGAAAAAABFdiK7oifWRb4ADuqgSRcnBwBBFDg0JJk7TY1fmsJrh7tNAAAAAAEJAABBFDg0JJk7TY1fmsJrh7tNAAFnbV%2FqAAA%3D&exvsurl=1&viewmodel=ReadMessageItem"
}

View File

@ -10,6 +10,3 @@ var EmailWithEventInfo string
//go:embed email-with-event-object.json //go:embed email-with-event-object.json
var EmailWithEventObject string var EmailWithEventObject string
//go:embed email-within-email.json
var EmailWithinEmail string

View File

@ -166,20 +166,3 @@ var GraphTimeZoneToTZ = map[string]string{
"Yukon Standard Time": "America/Whitehorse", "Yukon Standard Time": "America/Whitehorse",
"tzone://Microsoft/Utc": "Etc/UTC", "tzone://Microsoft/Utc": "Etc/UTC",
} }
// Map from alternatives to the canonical time zone name
// There mapping are currently generated by manually going on the
// values in the GraphTimeZoneToTZ which is not available in the tzdb
var CanonicalTimeZoneMap = map[string]string{
"Africa/Asmara": "Africa/Asmera",
"Asia/Calcutta": "Asia/Kolkata",
"Asia/Rangoon": "Asia/Yangon",
"Asia/Saigon": "Asia/Ho_Chi_Minh",
"Europe/Kiev": "Europe/Kyiv",
"Europe/Warsaw": "Europe/Warszawa",
"America/Buenos_Aires": "America/Argentina/Buenos_Aires",
"America/Godthab": "America/Nuuk",
// NOTE: "Atlantic/Raykjavik" missing in tzdb but is in MS list
"Etc/UTC": "UTC", // simplifying the time zone name
}

View File

@ -5,7 +5,6 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/mail"
"strings" "strings"
"time" "time"
"unicode" "unicode"
@ -17,7 +16,6 @@ import (
"github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/common/str" "github.com/alcionai/corso/src/internal/common/str"
"github.com/alcionai/corso/src/internal/converters/ics/tzdata"
"github.com/alcionai/corso/src/pkg/dttm" "github.com/alcionai/corso/src/pkg/dttm"
"github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/services/m365/api" "github.com/alcionai/corso/src/pkg/services/m365/api"
@ -33,8 +31,7 @@ import (
// TODO locations: https://github.com/alcionai/corso/issues/5003 // TODO locations: https://github.com/alcionai/corso/issues/5003
const ( const (
ICalDateTimeFormat = "20060102T150405" ICalDateTimeFormat = "20060102T150405Z"
ICalDateTimeFormatUTC = "20060102T150405Z"
ICalDateFormat = "20060102" ICalDateFormat = "20060102"
) )
@ -175,17 +172,6 @@ func getRecurrencePattern(
recurComponents = append(recurComponents, "BYDAY="+prefix+strings.Join(dowComponents, ",")) recurComponents = append(recurComponents, "BYDAY="+prefix+strings.Join(dowComponents, ","))
} }
// This is necessary to compute when weekly events recur
fdow := pat.GetFirstDayOfWeek()
if fdow != nil {
icalday, ok := GraphToICalDOW[fdow.String()]
if !ok {
return "", clues.NewWC(ctx, "unknown first day of week").With("day", fdow)
}
recurComponents = append(recurComponents, "WKST="+icalday)
}
rrange := recurrence.GetRangeEscaped() rrange := recurrence.GetRangeEscaped()
if rrange != nil { if rrange != nil {
switch ptr.Val(rrange.GetTypeEscaped()) { switch ptr.Val(rrange.GetTypeEscaped()) {
@ -209,7 +195,7 @@ func getRecurrencePattern(
return "", clues.WrapWC(ctx, err, "parsing end time") return "", clues.WrapWC(ctx, err, "parsing end time")
} }
recurComponents = append(recurComponents, "UNTIL="+endTime.Format(ICalDateTimeFormatUTC)) recurComponents = append(recurComponents, "UNTIL="+endTime.Format(ICalDateTimeFormat))
} }
case models.NOEND_RECURRENCERANGETYPE: case models.NOEND_RECURRENCERANGETYPE:
// Nothing to do // Nothing to do
@ -238,15 +224,10 @@ func FromEventable(ctx context.Context, event models.Eventable) (string, error)
cal := ics.NewCalendar() cal := ics.NewCalendar()
cal.SetProductId("-//Alcion//Corso") // Does this have to be customizable? cal.SetProductId("-//Alcion//Corso") // Does this have to be customizable?
err := addTimeZoneComponents(ctx, cal, event)
if err != nil {
return "", clues.Wrap(err, "adding timezone components")
}
id := ptr.Val(event.GetId()) id := ptr.Val(event.GetId())
iCalEvent := cal.AddEvent(id) iCalEvent := cal.AddEvent(id)
err = updateEventProperties(ctx, event, iCalEvent) err := updateEventProperties(ctx, event, iCalEvent)
if err != nil { if err != nil {
return "", clues.Wrap(err, "updating event properties") return "", clues.Wrap(err, "updating event properties")
} }
@ -277,7 +258,7 @@ func FromEventable(ctx context.Context, event models.Eventable) (string, error)
exICalEvent := cal.AddEvent(id) exICalEvent := cal.AddEvent(id)
start := exception.GetOriginalStart() // will always be in UTC start := exception.GetOriginalStart() // will always be in UTC
exICalEvent.AddProperty(ics.ComponentProperty(ics.PropertyRecurrenceId), start.Format(ICalDateTimeFormatUTC)) exICalEvent.AddProperty(ics.ComponentProperty(ics.PropertyRecurrenceId), start.Format(ICalDateTimeFormat))
err = updateEventProperties(ctx, exception, exICalEvent) err = updateEventProperties(ctx, exception, exICalEvent)
if err != nil { if err != nil {
@ -288,91 +269,6 @@ func FromEventable(ctx context.Context, event models.Eventable) (string, error)
return cal.Serialize(), nil return cal.Serialize(), nil
} }
func getTZDataKeyValues(ctx context.Context, timezone string) (map[string]string, error) {
template, ok := tzdata.TZData[timezone]
if !ok {
return nil, clues.NewWC(ctx, "timezone not found in tz database").
With("timezone", timezone)
}
keyValues := map[string]string{}
for _, line := range strings.Split(template, "\n") {
splits := strings.SplitN(line, ":", 2)
if len(splits) != 2 {
return nil, clues.NewWC(ctx, "invalid tzdata line").
With("line", line).
With("timezone", timezone)
}
keyValues[splits[0]] = splits[1]
}
return keyValues, nil
}
func addTimeZoneComponents(ctx context.Context, cal *ics.Calendar, event models.Eventable) error {
// Handling of timezone get a bit tricky when we have to deal with
// relative recurrence. The issue comes up when we set a recurrence
// to be something like "repeat every 3rd Tuesday". Tuesday in UTC
// and in IST will be different and so we cannot just always use UTC.
//
// The way this is solved is by using the timezone in the
// recurrence for start and end timezones as we have to use UTC
// for UNTIL(mostly).
// https://www.rfc-editor.org/rfc/rfc5545#section-3.3.10
timezone, err := getRecurrenceTimezone(ctx, event)
if err != nil {
return clues.Stack(err)
}
if timezone != time.UTC {
kvs, err := getTZDataKeyValues(ctx, timezone.String())
if err != nil {
return clues.Stack(err)
}
tz := cal.AddTimezone(timezone.String())
for k, v := range kvs {
tz.AddProperty(ics.ComponentProperty(k), v)
}
}
return nil
}
// getRecurrenceTimezone get the timezone specified by the recurrence
// in the calendar. It does a normalization pass where we always convert
// the timezone to the value in tzdb If we don't have a recurrence
// timezone, we don't have to use a specific timezone in the export and
// is safe to return UTC from this method.
func getRecurrenceTimezone(ctx context.Context, event models.Eventable) (*time.Location, error) {
if event.GetRecurrence() != nil {
timezone := ptr.Val(event.GetRecurrence().GetRangeEscaped().GetRecurrenceTimeZone())
ctz, ok := GraphTimeZoneToTZ[timezone]
if ok {
timezone = ctz
}
cannon, ok := CanonicalTimeZoneMap[timezone]
if ok {
timezone = cannon
}
loc, err := time.LoadLocation(timezone)
if err != nil {
return nil, clues.WrapWC(ctx, err, "unknown timezone").
With("timezone", timezone)
}
return loc, nil
}
return time.UTC, nil
}
func isASCII(s string) bool { func isASCII(s string) bool {
for _, c := range s { for _, c := range s {
if c > unicode.MaxASCII { if c > unicode.MaxASCII {
@ -383,12 +279,6 @@ func isASCII(s string) bool {
return true return true
} }
// Checks if a given string is a valid email address
func isEmail(em string) bool {
_, err := mail.ParseAddress(em)
return err == nil
}
func updateEventProperties(ctx context.Context, event models.Eventable, iCalEvent *ics.VEvent) error { func updateEventProperties(ctx context.Context, event models.Eventable, iCalEvent *ics.VEvent) error {
// CREATED - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.1 // CREATED - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.1
created := event.GetCreatedDateTime() created := event.GetCreatedDateTime()
@ -402,11 +292,6 @@ func updateEventProperties(ctx context.Context, event models.Eventable, iCalEven
iCalEvent.SetModifiedAt(ptr.Val(modified)) iCalEvent.SetModifiedAt(ptr.Val(modified))
} }
timezone, err := getRecurrenceTimezone(ctx, event)
if err != nil {
return err
}
// DTSTART - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.4 // DTSTART - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.4
allDay := ptr.Val(event.GetIsAllDay()) allDay := ptr.Val(event.GetIsAllDay())
startString := event.GetStart().GetDateTime() startString := event.GetStart().GetDateTime()
@ -418,7 +303,11 @@ func updateEventProperties(ctx context.Context, event models.Eventable, iCalEven
return clues.WrapWC(ctx, err, "parsing start time") return clues.WrapWC(ctx, err, "parsing start time")
} }
addTime(iCalEvent, ics.ComponentPropertyDtStart, start, allDay, timezone) if allDay {
iCalEvent.SetStartAt(start, ics.WithValue(string(ics.ValueDataTypeDate)))
} else {
iCalEvent.SetStartAt(start)
}
} }
// DTEND - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.2 // DTEND - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.2
@ -431,7 +320,11 @@ func updateEventProperties(ctx context.Context, event models.Eventable, iCalEven
return clues.WrapWC(ctx, err, "parsing end time") return clues.WrapWC(ctx, err, "parsing end time")
} }
addTime(iCalEvent, ics.ComponentPropertyDtEnd, end, allDay, timezone) if allDay {
iCalEvent.SetEndAt(end, ics.WithValue(string(ics.ValueDataTypeDate)))
} else {
iCalEvent.SetEndAt(end)
}
} }
recurrence := event.GetRecurrence() recurrence := event.GetRecurrence()
@ -484,14 +377,7 @@ func updateEventProperties(ctx context.Context, event models.Eventable, iCalEven
desc := replacer.Replace(description) desc := replacer.Replace(description)
iCalEvent.AddProperty("X-ALT-DESC", desc, ics.WithFmtType("text/html")) iCalEvent.AddProperty("X-ALT-DESC", desc, ics.WithFmtType("text/html"))
} else { } else {
// Disable auto wrap, causes huge memory spikes stripped, err := html2text.FromString(description, html2text.Options{PrettyTables: true})
// https://github.com/jaytaylor/html2text/issues/48
prettyTablesOptions := html2text.NewPrettyTablesOptions()
prettyTablesOptions.AutoWrapText = false
stripped, err := html2text.FromString(
description,
html2text.Options{PrettyTables: true, PrettyTablesOptions: prettyTablesOptions})
if err != nil { if err != nil {
return clues.Wrap(err, "converting html to text"). return clues.Wrap(err, "converting html to text").
With("description_length", len(description)) With("description_length", len(description))
@ -595,21 +481,8 @@ func updateEventProperties(ctx context.Context, event models.Eventable, iCalEven
} }
} }
// It is possible that we get non email items like the below
// one which is an internal representation of the user in the
// Exchange system. While we can technically output this as an
// attendee, it is not useful plus other downstream tools like
// ones to use PST can choke on this.
// /o=ExchangeLabs/ou=ExchangeAdministrative Group(FY...LT)/cn=Recipients/cn=883...4a-John Doe
addr := ptr.Val(attendee.GetEmailAddress().GetAddress()) addr := ptr.Val(attendee.GetEmailAddress().GetAddress())
if isEmail(addr) {
iCalEvent.AddAttendee(addr, props...) iCalEvent.AddAttendee(addr, props...)
} else {
logger.Ctx(ctx).
With("attendee_email", addr).
With("attendee_name", name).
Info("skipping non email attendee from ics export")
}
} }
// LOCATION - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.7 // LOCATION - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.7
@ -737,26 +610,6 @@ func updateEventProperties(ctx context.Context, event models.Eventable, iCalEven
return nil return nil
} }
func addTime(iCalEvent *ics.VEvent, prop ics.ComponentProperty, tm time.Time, allDay bool, tzLoc *time.Location) {
if allDay {
if tzLoc == time.UTC {
iCalEvent.SetProperty(prop, tm.Format(ICalDateFormat), ics.WithValue(string(ics.ValueDataTypeDate)))
} else {
iCalEvent.SetProperty(
prop,
tm.In(tzLoc).Format(ICalDateFormat),
ics.WithValue(string(ics.ValueDataTypeDate)),
keyValues("TZID", tzLoc.String()))
}
} else {
if tzLoc == time.UTC {
iCalEvent.SetProperty(prop, tm.Format(ICalDateTimeFormatUTC))
} else {
iCalEvent.SetProperty(prop, tm.In(tzLoc).Format(ICalDateTimeFormat), keyValues("TZID", tzLoc.String()))
}
}
}
func getCancelledDates(ctx context.Context, event models.Eventable) ([]time.Time, error) { func getCancelledDates(ctx context.Context, event models.Eventable) ([]time.Time, error) {
dateStrings, err := api.GetCancelledEventDateStrings(event) dateStrings, err := api.GetCancelledEventDateStrings(event)
if err != nil { if err != nil {

View File

@ -13,7 +13,6 @@ import (
"testing" "testing"
"time" "time"
ics "github.com/arran4/golang-ical"
"github.com/microsoft/kiota-abstractions-go/serialization" "github.com/microsoft/kiota-abstractions-go/serialization"
kjson "github.com/microsoft/kiota-serialization-json-go" kjson "github.com/microsoft/kiota-serialization-json-go"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
@ -22,7 +21,6 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/converters/ics/tzdata"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
) )
@ -34,7 +32,7 @@ func TestICSUnitSuite(t *testing.T) {
suite.Run(t, &ICSUnitSuite{Suite: tester.NewUnitSuite(t)}) suite.Run(t, &ICSUnitSuite{Suite: tester.NewUnitSuite(t)})
} }
func (s *ICSUnitSuite) TestGetLocationString() { func (suite *ICSUnitSuite) TestGetLocationString() {
table := []struct { table := []struct {
name string name string
loc func() models.Locationable loc func() models.Locationable
@ -112,13 +110,13 @@ func (s *ICSUnitSuite) TestGetLocationString() {
} }
for _, tt := range table { for _, tt := range table {
s.Run(tt.name, func() { suite.Run(tt.name, func() {
assert.Equal(s.T(), tt.expect, getLocationString(tt.loc())) assert.Equal(suite.T(), tt.expect, getLocationString(tt.loc()))
}) })
} }
} }
func (s *ICSUnitSuite) TestGetUTCTime() { func (suite *ICSUnitSuite) TestGetUTCTime() {
table := []struct { table := []struct {
name string name string
timestamp string timestamp string
@ -164,18 +162,18 @@ func (s *ICSUnitSuite) TestGetUTCTime() {
} }
for _, tt := range table { for _, tt := range table {
s.Run(tt.name, func() { suite.Run(tt.name, func() {
t, err := GetUTCTime(tt.timestamp, tt.timezone) t, err := GetUTCTime(tt.timestamp, tt.timezone)
tt.errCheck(s.T(), err) tt.errCheck(suite.T(), err)
if !tt.time.Equal(time.Time{}) { if !tt.time.Equal(time.Time{}) {
assert.Equal(s.T(), tt.time, t) assert.Equal(suite.T(), tt.time, t)
} }
}) })
} }
} }
func (s *ICSUnitSuite) TestGetRecurrencePattern() { func (suite *ICSUnitSuite) TestGetRecurrencePattern() {
table := []struct { table := []struct {
name string name string
recurrence func() models.PatternedRecurrenceable recurrence func() models.PatternedRecurrenceable
@ -189,37 +187,16 @@ func (s *ICSUnitSuite) TestGetRecurrencePattern() {
pat := models.NewRecurrencePattern() pat := models.NewRecurrencePattern()
typ, err := models.ParseRecurrencePatternType("daily") typ, err := models.ParseRecurrencePatternType("daily")
require.NoError(s.T(), err) require.NoError(suite.T(), err)
pat.SetTypeEscaped(typ.(*models.RecurrencePatternType)) pat.SetTypeEscaped(typ.(*models.RecurrencePatternType))
pat.SetInterval(ptr.To(int32(1))) pat.SetInterval(ptr.To(int32(1)))
pat.SetFirstDayOfWeek(ptr.To(models.SUNDAY_DAYOFWEEK))
rec.SetPattern(pat) rec.SetPattern(pat)
return rec return rec
}, },
expect: "FREQ=DAILY;INTERVAL=1;WKST=SU", expect: "FREQ=DAILY;INTERVAL=1",
errCheck: require.NoError,
},
{
name: "daily different start of week",
recurrence: func() models.PatternedRecurrenceable {
rec := models.NewPatternedRecurrence()
pat := models.NewRecurrencePattern()
typ, err := models.ParseRecurrencePatternType("daily")
require.NoError(s.T(), err)
pat.SetTypeEscaped(typ.(*models.RecurrencePatternType))
pat.SetInterval(ptr.To(int32(1)))
pat.SetFirstDayOfWeek(ptr.To(models.MONDAY_DAYOFWEEK))
rec.SetPattern(pat)
return rec
},
expect: "FREQ=DAILY;INTERVAL=1;WKST=MO",
errCheck: require.NoError, errCheck: require.NoError,
}, },
{ {
@ -229,16 +206,15 @@ func (s *ICSUnitSuite) TestGetRecurrencePattern() {
pat := models.NewRecurrencePattern() pat := models.NewRecurrencePattern()
typ, err := models.ParseRecurrencePatternType("daily") typ, err := models.ParseRecurrencePatternType("daily")
require.NoError(s.T(), err) require.NoError(suite.T(), err)
pat.SetTypeEscaped(typ.(*models.RecurrencePatternType)) pat.SetTypeEscaped(typ.(*models.RecurrencePatternType))
pat.SetInterval(ptr.To(int32(1))) pat.SetInterval(ptr.To(int32(1)))
pat.SetFirstDayOfWeek(ptr.To(models.SUNDAY_DAYOFWEEK))
rng := models.NewRecurrenceRange() rng := models.NewRecurrenceRange()
rrtype, err := models.ParseRecurrenceRangeType("endDate") rrtype, err := models.ParseRecurrenceRangeType("endDate")
require.NoError(s.T(), err) require.NoError(suite.T(), err)
rng.SetTypeEscaped(rrtype.(*models.RecurrenceRangeType)) rng.SetTypeEscaped(rrtype.(*models.RecurrenceRangeType))
@ -251,7 +227,7 @@ func (s *ICSUnitSuite) TestGetRecurrencePattern() {
return rec return rec
}, },
expect: "FREQ=DAILY;INTERVAL=1;WKST=SU;UNTIL=20210101T182959Z", expect: "FREQ=DAILY;INTERVAL=1;UNTIL=20210101T182959Z",
errCheck: require.NoError, errCheck: require.NoError,
}, },
{ {
@ -261,17 +237,16 @@ func (s *ICSUnitSuite) TestGetRecurrencePattern() {
pat := models.NewRecurrencePattern() pat := models.NewRecurrencePattern()
typ, err := models.ParseRecurrencePatternType("weekly") typ, err := models.ParseRecurrencePatternType("weekly")
require.NoError(s.T(), err) require.NoError(suite.T(), err)
pat.SetTypeEscaped(typ.(*models.RecurrencePatternType)) pat.SetTypeEscaped(typ.(*models.RecurrencePatternType))
pat.SetInterval(ptr.To(int32(1))) pat.SetInterval(ptr.To(int32(1)))
pat.SetFirstDayOfWeek(ptr.To(models.SUNDAY_DAYOFWEEK))
rec.SetPattern(pat) rec.SetPattern(pat)
return rec return rec
}, },
expect: "FREQ=WEEKLY;INTERVAL=1;WKST=SU", expect: "FREQ=WEEKLY;INTERVAL=1",
errCheck: require.NoError, errCheck: require.NoError,
}, },
{ {
@ -281,16 +256,15 @@ func (s *ICSUnitSuite) TestGetRecurrencePattern() {
pat := models.NewRecurrencePattern() pat := models.NewRecurrencePattern()
typ, err := models.ParseRecurrencePatternType("weekly") typ, err := models.ParseRecurrencePatternType("weekly")
require.NoError(s.T(), err) require.NoError(suite.T(), err)
pat.SetTypeEscaped(typ.(*models.RecurrencePatternType)) pat.SetTypeEscaped(typ.(*models.RecurrencePatternType))
pat.SetInterval(ptr.To(int32(1))) pat.SetInterval(ptr.To(int32(1)))
pat.SetFirstDayOfWeek(ptr.To(models.SUNDAY_DAYOFWEEK))
rng := models.NewRecurrenceRange() rng := models.NewRecurrenceRange()
rrtype, err := models.ParseRecurrenceRangeType("endDate") rrtype, err := models.ParseRecurrenceRangeType("endDate")
require.NoError(s.T(), err) require.NoError(suite.T(), err)
rng.SetTypeEscaped(rrtype.(*models.RecurrenceRangeType)) rng.SetTypeEscaped(rrtype.(*models.RecurrenceRangeType))
@ -303,7 +277,7 @@ func (s *ICSUnitSuite) TestGetRecurrencePattern() {
return rec return rec
}, },
expect: "FREQ=WEEKLY;INTERVAL=1;WKST=SU;UNTIL=20210101T235959Z", expect: "FREQ=WEEKLY;INTERVAL=1;UNTIL=20210101T235959Z",
errCheck: require.NoError, errCheck: require.NoError,
}, },
{ {
@ -313,16 +287,15 @@ func (s *ICSUnitSuite) TestGetRecurrencePattern() {
pat := models.NewRecurrencePattern() pat := models.NewRecurrencePattern()
typ, err := models.ParseRecurrencePatternType("weekly") typ, err := models.ParseRecurrencePatternType("weekly")
require.NoError(s.T(), err) require.NoError(suite.T(), err)
pat.SetTypeEscaped(typ.(*models.RecurrencePatternType)) pat.SetTypeEscaped(typ.(*models.RecurrencePatternType))
pat.SetInterval(ptr.To(int32(1))) pat.SetInterval(ptr.To(int32(1)))
pat.SetFirstDayOfWeek(ptr.To(models.SUNDAY_DAYOFWEEK))
rng := models.NewRecurrenceRange() rng := models.NewRecurrenceRange()
rrtype, err := models.ParseRecurrenceRangeType("numbered") rrtype, err := models.ParseRecurrenceRangeType("numbered")
require.NoError(s.T(), err) require.NoError(suite.T(), err)
rng.SetTypeEscaped(rrtype.(*models.RecurrenceRangeType)) rng.SetTypeEscaped(rrtype.(*models.RecurrenceRangeType))
@ -334,7 +307,7 @@ func (s *ICSUnitSuite) TestGetRecurrencePattern() {
return rec return rec
}, },
expect: "FREQ=WEEKLY;INTERVAL=1;WKST=SU;COUNT=10", expect: "FREQ=WEEKLY;INTERVAL=1;COUNT=10",
errCheck: require.NoError, errCheck: require.NoError,
}, },
{ {
@ -344,11 +317,10 @@ func (s *ICSUnitSuite) TestGetRecurrencePattern() {
pat := models.NewRecurrencePattern() pat := models.NewRecurrencePattern()
typ, err := models.ParseRecurrencePatternType("weekly") typ, err := models.ParseRecurrencePatternType("weekly")
require.NoError(s.T(), err) require.NoError(suite.T(), err)
pat.SetTypeEscaped(typ.(*models.RecurrencePatternType)) pat.SetTypeEscaped(typ.(*models.RecurrencePatternType))
pat.SetInterval(ptr.To(int32(1))) pat.SetInterval(ptr.To(int32(1)))
pat.SetFirstDayOfWeek(ptr.To(models.SUNDAY_DAYOFWEEK))
days := []models.DayOfWeek{ days := []models.DayOfWeek{
models.MONDAY_DAYOFWEEK, models.MONDAY_DAYOFWEEK,
@ -362,7 +334,7 @@ func (s *ICSUnitSuite) TestGetRecurrencePattern() {
return rec return rec
}, },
expect: "FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,WE,TH;WKST=SU", expect: "FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,WE,TH",
errCheck: require.NoError, errCheck: require.NoError,
}, },
{ {
@ -372,17 +344,16 @@ func (s *ICSUnitSuite) TestGetRecurrencePattern() {
pat := models.NewRecurrencePattern() pat := models.NewRecurrencePattern()
typ, err := models.ParseRecurrencePatternType("daily") typ, err := models.ParseRecurrencePatternType("daily")
require.NoError(s.T(), err) require.NoError(suite.T(), err)
pat.SetTypeEscaped(typ.(*models.RecurrencePatternType)) pat.SetTypeEscaped(typ.(*models.RecurrencePatternType))
pat.SetInterval(ptr.To(int32(2))) pat.SetInterval(ptr.To(int32(2)))
pat.SetFirstDayOfWeek(ptr.To(models.SUNDAY_DAYOFWEEK))
rec.SetPattern(pat) rec.SetPattern(pat)
return rec return rec
}, },
expect: "FREQ=DAILY;INTERVAL=2;WKST=SU", expect: "FREQ=DAILY;INTERVAL=2",
errCheck: require.NoError, errCheck: require.NoError,
}, },
{ {
@ -392,11 +363,10 @@ func (s *ICSUnitSuite) TestGetRecurrencePattern() {
pat := models.NewRecurrencePattern() pat := models.NewRecurrencePattern()
typ, err := models.ParseRecurrencePatternType("absoluteMonthly") typ, err := models.ParseRecurrencePatternType("absoluteMonthly")
require.NoError(s.T(), err) require.NoError(suite.T(), err)
pat.SetTypeEscaped(typ.(*models.RecurrencePatternType)) pat.SetTypeEscaped(typ.(*models.RecurrencePatternType))
pat.SetInterval(ptr.To(int32(1))) pat.SetInterval(ptr.To(int32(1)))
pat.SetFirstDayOfWeek(ptr.To(models.SUNDAY_DAYOFWEEK))
pat.SetDayOfMonth(ptr.To(int32(5))) pat.SetDayOfMonth(ptr.To(int32(5)))
@ -404,7 +374,7 @@ func (s *ICSUnitSuite) TestGetRecurrencePattern() {
return rec return rec
}, },
expect: "FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=5;WKST=SU", expect: "FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=5",
errCheck: require.NoError, errCheck: require.NoError,
}, },
{ {
@ -414,11 +384,10 @@ func (s *ICSUnitSuite) TestGetRecurrencePattern() {
pat := models.NewRecurrencePattern() pat := models.NewRecurrencePattern()
typ, err := models.ParseRecurrencePatternType("absoluteYearly") typ, err := models.ParseRecurrencePatternType("absoluteYearly")
require.NoError(s.T(), err) require.NoError(suite.T(), err)
pat.SetTypeEscaped(typ.(*models.RecurrencePatternType)) pat.SetTypeEscaped(typ.(*models.RecurrencePatternType))
pat.SetInterval(ptr.To(int32(3))) pat.SetInterval(ptr.To(int32(3)))
pat.SetFirstDayOfWeek(ptr.To(models.SUNDAY_DAYOFWEEK))
pat.SetMonth(ptr.To(int32(8))) pat.SetMonth(ptr.To(int32(8)))
@ -426,7 +395,7 @@ func (s *ICSUnitSuite) TestGetRecurrencePattern() {
return rec return rec
}, },
expect: "FREQ=YEARLY;INTERVAL=3;BYMONTH=8;WKST=SU", expect: "FREQ=YEARLY;INTERVAL=3;BYMONTH=8",
errCheck: require.NoError, errCheck: require.NoError,
}, },
{ {
@ -436,38 +405,37 @@ func (s *ICSUnitSuite) TestGetRecurrencePattern() {
pat := models.NewRecurrencePattern() pat := models.NewRecurrencePattern()
typ, err := models.ParseRecurrencePatternType("relativeYearly") typ, err := models.ParseRecurrencePatternType("relativeYearly")
require.NoError(s.T(), err) require.NoError(suite.T(), err)
pat.SetTypeEscaped(typ.(*models.RecurrencePatternType)) pat.SetTypeEscaped(typ.(*models.RecurrencePatternType))
pat.SetInterval(ptr.To(int32(1))) pat.SetInterval(ptr.To(int32(1)))
pat.SetFirstDayOfWeek(ptr.To(models.SUNDAY_DAYOFWEEK))
pat.SetMonth(ptr.To(int32(8))) pat.SetMonth(ptr.To(int32(8)))
pat.SetDaysOfWeek([]models.DayOfWeek{models.FRIDAY_DAYOFWEEK}) pat.SetDaysOfWeek([]models.DayOfWeek{models.FRIDAY_DAYOFWEEK})
wi, err := models.ParseWeekIndex("first") wi, err := models.ParseWeekIndex("first")
require.NoError(s.T(), err) require.NoError(suite.T(), err)
pat.SetIndex(wi.(*models.WeekIndex)) pat.SetIndex(wi.(*models.WeekIndex))
rec.SetPattern(pat) rec.SetPattern(pat)
return rec return rec
}, },
expect: "FREQ=YEARLY;INTERVAL=1;BYMONTH=8;BYDAY=1FR;WKST=SU", expect: "FREQ=YEARLY;INTERVAL=1;BYMONTH=8;BYDAY=1FR",
errCheck: require.NoError, errCheck: require.NoError,
}, },
// TODO(meain): could still use more tests for edge cases of time // TODO(meain): could still use more tests for edge cases of time
} }
for _, tt := range table { for _, tt := range table {
s.Run(tt.name, func() { suite.Run(tt.name, func() {
ctx, flush := tester.NewContext(s.T()) ctx, flush := tester.NewContext(suite.T())
defer flush() defer flush()
rec, err := getRecurrencePattern(ctx, tt.recurrence()) rec, err := getRecurrencePattern(ctx, tt.recurrence())
tt.errCheck(s.T(), err) tt.errCheck(suite.T(), err)
assert.Equal(s.T(), tt.expect, rec) assert.Equal(suite.T(), tt.expect, rec)
}) })
} }
} }
@ -492,8 +460,8 @@ func baseEvent() *models.Event {
return e return e
} }
func (s *ICSUnitSuite) TestEventConversion() { func (suite *ICSUnitSuite) TestEventConversion() {
t := s.T() t := suite.T()
table := []struct { table := []struct {
name string name string
@ -578,19 +546,14 @@ func (s *ICSUnitSuite) TestEventConversion() {
rec := models.NewPatternedRecurrence() rec := models.NewPatternedRecurrence()
pat := models.NewRecurrencePattern() pat := models.NewRecurrencePattern()
rng := models.NewRecurrenceRange()
typ, err := models.ParseRecurrencePatternType("daily") typ, err := models.ParseRecurrencePatternType("daily")
require.NoError(t, err) require.NoError(t, err)
pat.SetTypeEscaped(typ.(*models.RecurrencePatternType)) pat.SetTypeEscaped(typ.(*models.RecurrencePatternType))
pat.SetInterval(ptr.To(int32(1))) pat.SetInterval(ptr.To(int32(1)))
pat.SetFirstDayOfWeek(ptr.To(models.SUNDAY_DAYOFWEEK))
rng.SetRecurrenceTimeZone(ptr.To("UTC"))
rec.SetPattern(pat) rec.SetPattern(pat)
rec.SetRangeEscaped(rng)
e.SetRecurrence(rec) e.SetRecurrence(rec)
@ -867,8 +830,8 @@ func (s *ICSUnitSuite) TestEventConversion() {
} }
for _, tt := range table { for _, tt := range table {
s.Run(tt.name, func() { suite.Run(tt.name, func() {
t := s.T() t := suite.T()
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
@ -918,8 +881,8 @@ func checkAttendee(t *testing.T, out, check, msg string) {
assert.ElementsMatch(t, as, bs, fmt.Sprintf("fields %s", msg)) assert.ElementsMatch(t, as, bs, fmt.Sprintf("fields %s", msg))
} }
func (s *ICSUnitSuite) TestAttendees() { func (suite *ICSUnitSuite) TestAttendees() {
t := s.T() t := suite.T()
table := []struct { table := []struct {
name string name string
@ -945,17 +908,6 @@ func (s *ICSUnitSuite) TestAttendees() {
"attendee") "attendee")
}, },
}, },
{
name: "attendee with internal exchange representation for email",
att: [][]string{{
"/o=ExchangeLabs/ou=ExchangeAdministrative Group(FY...LT)/cn=Recipients/cn=883...4a-John Doe",
"required",
"declined",
}},
check: func(out string) {
assert.NotContains(t, out, "ATTENDEE")
},
},
{ {
name: "multiple attendees", name: "multiple attendees",
att: [][]string{ att: [][]string{
@ -986,8 +938,8 @@ func (s *ICSUnitSuite) TestAttendees() {
} }
for _, tt := range table { for _, tt := range table {
s.Run(tt.name, func() { suite.Run(tt.name, func() {
t := s.T() t := suite.T()
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
@ -1108,8 +1060,8 @@ func checkAttachment(t *testing.T, out, check, msg string) {
assert.ElementsMatch(t, as, bs, fmt.Sprintf("fields %s", msg)) assert.ElementsMatch(t, as, bs, fmt.Sprintf("fields %s", msg))
} }
func (s *ICSUnitSuite) TestAttachments() { func (suite *ICSUnitSuite) TestAttachments() {
t := s.T() t := suite.T()
type attachment struct { type attachment struct {
cid string // contentid cid string // contentid
@ -1165,8 +1117,8 @@ func (s *ICSUnitSuite) TestAttachments() {
} }
for _, tt := range table { for _, tt := range table {
s.Run(tt.name, func() { suite.Run(tt.name, func() {
t := s.T() t := suite.T()
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
@ -1209,7 +1161,7 @@ func (s *ICSUnitSuite) TestAttachments() {
} }
} }
func (s *ICSUnitSuite) TestCancellations() { func (suite *ICSUnitSuite) TestCancellations() {
table := []struct { table := []struct {
name string name string
cancelledIds []string cancelledIds []string
@ -1233,8 +1185,8 @@ func (s *ICSUnitSuite) TestCancellations() {
} }
for _, tt := range table { for _, tt := range table {
s.Run(tt.name, func() { suite.Run(tt.name, func() {
t := s.T() t := suite.T()
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
@ -1297,7 +1249,7 @@ func eventToJSON(e *models.Event) ([]byte, error) {
return bts, err return bts, err
} }
func (s *ICSUnitSuite) TestEventExceptions() { func (suite *ICSUnitSuite) TestEventExceptions() {
table := []struct { table := []struct {
name string name string
event func() *models.Event event func() *models.Event
@ -1319,7 +1271,7 @@ func (s *ICSUnitSuite) TestEventExceptions() {
exception.SetEnd(newEnd) exception.SetEnd(newEnd)
parsed, err := eventToMap(exception) parsed, err := eventToMap(exception)
require.NoError(s.T(), err, "parsing exception") require.NoError(suite.T(), err, "parsing exception")
// add exception event to additional data // add exception event to additional data
e.SetAdditionalData(map[string]any{ e.SetAdditionalData(map[string]any{
@ -1338,15 +1290,15 @@ func (s *ICSUnitSuite) TestEventExceptions() {
} }
} }
assert.Equal(s.T(), 2, events, "number of events") assert.Equal(suite.T(), 2, events, "number of events")
assert.Contains(s.T(), out, "RECURRENCE-ID:20210101T120000Z", "recurrence id") assert.Contains(suite.T(), out, "RECURRENCE-ID:20210101T120000Z", "recurrence id")
assert.Contains(s.T(), out, "SUMMARY:Subject", "original event") assert.Contains(suite.T(), out, "SUMMARY:Subject", "original event")
assert.Contains(s.T(), out, "SUMMARY:Exception", "exception event") assert.Contains(suite.T(), out, "SUMMARY:Exception", "exception event")
assert.Contains(s.T(), out, "DTSTART:20210101T130000Z", "new start time") assert.Contains(suite.T(), out, "DTSTART:20210101T130000Z", "new start time")
assert.Contains(s.T(), out, "DTEND:20210101T140000Z", "new end time") assert.Contains(suite.T(), out, "DTEND:20210101T140000Z", "new end time")
}, },
}, },
{ {
@ -1375,10 +1327,10 @@ func (s *ICSUnitSuite) TestEventExceptions() {
exception2.SetEnd(newEnd) exception2.SetEnd(newEnd)
parsed1, err := eventToMap(exception1) parsed1, err := eventToMap(exception1)
require.NoError(s.T(), err, "parsing exception 1") require.NoError(suite.T(), err, "parsing exception 1")
parsed2, err := eventToMap(exception2) parsed2, err := eventToMap(exception2)
require.NoError(s.T(), err, "parsing exception 2") require.NoError(suite.T(), err, "parsing exception 2")
// add exception event to additional data // add exception event to additional data
e.SetAdditionalData(map[string]any{ e.SetAdditionalData(map[string]any{
@ -1397,230 +1349,36 @@ func (s *ICSUnitSuite) TestEventExceptions() {
} }
} }
assert.Equal(s.T(), 3, events, "number of events") assert.Equal(suite.T(), 3, events, "number of events")
assert.Contains(s.T(), out, "RECURRENCE-ID:20210101T120000Z", "recurrence id 1") assert.Contains(suite.T(), out, "RECURRENCE-ID:20210101T120000Z", "recurrence id 1")
assert.Contains(s.T(), out, "RECURRENCE-ID:20210102T120000Z", "recurrence id 2") assert.Contains(suite.T(), out, "RECURRENCE-ID:20210102T120000Z", "recurrence id 2")
assert.Contains(s.T(), out, "SUMMARY:Subject", "original event") assert.Contains(suite.T(), out, "SUMMARY:Subject", "original event")
assert.Contains(s.T(), out, "SUMMARY:Exception 1", "exception event 1") assert.Contains(suite.T(), out, "SUMMARY:Exception 1", "exception event 1")
assert.Contains(s.T(), out, "SUMMARY:Exception 2", "exception event 2") assert.Contains(suite.T(), out, "SUMMARY:Exception 2", "exception event 2")
assert.Contains(s.T(), out, "DTSTART:20210101T130000Z", "new start time 1") assert.Contains(suite.T(), out, "DTSTART:20210101T130000Z", "new start time 1")
assert.Contains(s.T(), out, "DTEND:20210101T140000Z", "new end time 1") assert.Contains(suite.T(), out, "DTEND:20210101T140000Z", "new end time 1")
assert.Contains(s.T(), out, "DTSTART:20210102T130000Z", "new start time 2") assert.Contains(suite.T(), out, "DTSTART:20210102T130000Z", "new start time 2")
assert.Contains(s.T(), out, "DTEND:20210102T140000Z", "new end time 2") assert.Contains(suite.T(), out, "DTEND:20210102T140000Z", "new end time 2")
}, },
}, },
} }
for _, tt := range table { for _, tt := range table {
s.Run(tt.name, func() { suite.Run(tt.name, func() {
ctx, flush := tester.NewContext(s.T()) ctx, flush := tester.NewContext(suite.T())
defer flush() defer flush()
bts, err := eventToJSON(tt.event()) bts, err := eventToJSON(tt.event())
require.NoError(s.T(), err, "getting serialized content") require.NoError(suite.T(), err, "getting serialized content")
out, err := FromJSON(ctx, bts) out, err := FromJSON(ctx, bts)
require.NoError(s.T(), err, "converting to ics") require.NoError(suite.T(), err, "converting to ics")
tt.check(out) tt.check(out)
}) })
} }
} }
func (s *ICSUnitSuite) TestGetRecurrenceTimezone() {
table := []struct {
name string
intz string
outtz string
}{
{
name: "empty",
intz: "",
outtz: "UTC",
},
{
name: "utc",
intz: "UTC",
outtz: "UTC",
},
{
name: "simple",
intz: "Asia/Kolkata",
outtz: "Asia/Kolkata",
},
{
name: "windows tz",
intz: "India Standard Time",
outtz: "Asia/Kolkata",
},
{
name: "non canonical",
intz: "Asia/Calcutta",
outtz: "Asia/Kolkata",
},
}
for _, tt := range table {
s.Run(tt.name, func() {
ctx, flush := tester.NewContext(s.T())
defer flush()
event := baseEvent()
if len(tt.intz) > 0 {
recur := models.NewPatternedRecurrence()
rp := models.NewRecurrenceRange()
rp.SetRecurrenceTimeZone(ptr.To(tt.intz))
recur.SetRangeEscaped(rp)
event.SetRecurrence(recur)
}
timezone, err := getRecurrenceTimezone(ctx, event)
require.NoError(s.T(), err)
assert.Equal(s.T(), tt.outtz, timezone.String())
})
}
}
func (s *ICSUnitSuite) TestAddTimezoneComponents() {
event := baseEvent()
recur := models.NewPatternedRecurrence()
rp := models.NewRecurrenceRange()
rp.SetRecurrenceTimeZone(ptr.To("Asia/Kolkata"))
recur.SetRangeEscaped(rp)
event.SetRecurrence(recur)
ctx, flush := tester.NewContext(s.T())
defer flush()
cal := ics.NewCalendar()
err := addTimeZoneComponents(ctx, cal, event)
require.NoError(s.T(), err)
text := cal.Serialize()
assert.Contains(s.T(), text, "BEGIN:VTIMEZONE", "beginning of timezone")
assert.Contains(s.T(), text, "TZID:Asia/Kolkata", "timezone id")
assert.Contains(s.T(), text, "END:VTIMEZONE", "end of timezone")
}
func (s *ICSUnitSuite) TestAddTime() {
locak, err := time.LoadLocation("Asia/Kolkata")
require.NoError(s.T(), err)
table := []struct {
name string
prop ics.ComponentProperty
time time.Time
allDay bool
loc *time.Location
exp string
}{
{
name: "utc",
prop: ics.ComponentPropertyDtStart,
time: time.Date(2021, 1, 2, 3, 4, 5, 0, time.UTC),
allDay: false,
loc: time.UTC,
exp: "DTSTART:20210102T030405Z",
},
{
name: "local",
prop: ics.ComponentPropertyDtStart,
time: time.Date(2021, 1, 2, 3, 4, 5, 0, time.UTC),
allDay: false,
loc: locak,
exp: "DTSTART;TZID=Asia/Kolkata:20210102T083405",
},
{
name: "all day",
prop: ics.ComponentPropertyDtStart,
time: time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC),
allDay: true,
loc: time.UTC,
exp: "DTSTART;VALUE=DATE:20210102",
},
{
name: "all day local",
prop: ics.ComponentPropertyDtStart,
time: time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC),
allDay: true,
loc: locak,
exp: "DTSTART;VALUE=DATE;TZID=Asia/Kolkata:20210102",
},
{
name: "end",
prop: ics.ComponentPropertyDtEnd,
time: time.Date(2021, 1, 2, 3, 4, 5, 0, time.UTC),
allDay: false,
loc: time.UTC,
exp: "DTEND:20210102T030405Z",
},
{
// This won't happen, but a good test to have to test loc handling
name: "windows tz",
prop: ics.ComponentPropertyDtStart,
time: time.Date(2021, 1, 2, 3, 4, 5, 0, time.UTC),
allDay: false,
loc: time.FixedZone("India Standard Time", 5*60*60+30*60),
exp: "DTSTART;TZID=India Standard Time:20210102T083405",
},
}
for _, tt := range table {
s.Run(tt.name, func() {
cal := ics.NewCalendar()
evt := cal.AddEvent("id")
addTime(evt, tt.prop, tt.time, tt.allDay, tt.loc)
expSplits := strings.FieldsFunc(tt.exp, func(c rune) bool {
return c == ':' || c == ';'
})
text := cal.Serialize()
checkLine := ""
for _, l := range strings.Split(text, "\r\n") {
if strings.HasPrefix(l, string(tt.prop)) {
checkLine = l
break
}
}
actSplits := strings.FieldsFunc(checkLine, func(c rune) bool {
return c == ':' || c == ';'
})
assert.Greater(s.T(), len(checkLine), 0, "line not found")
assert.Equal(s.T(), len(expSplits), len(actSplits), "length of fields")
assert.ElementsMatch(s.T(), expSplits, actSplits, "fields")
})
}
}
// This tests and ensures that the generated data is int he format
// that we expect
func (s *ICSUnitSuite) TestGetTZDataKeyValues() {
for key := range tzdata.TZData {
s.Run(key, func() {
ctx, flush := tester.NewContext(s.T())
defer flush()
data, err := getTZDataKeyValues(ctx, key)
require.NoError(s.T(), err)
assert.NotEmpty(s.T(), data, "data")
assert.NotContains(s.T(), data, "BEGIN", "beginning of timezone") // should be stripped
assert.NotContains(s.T(), data, "END", "end of timezone") // should be stripped
assert.NotContains(s.T(), data, "TZID", "timezone id") // should be stripped
assert.Contains(s.T(), data, "DTSTART", "start time")
assert.Contains(s.T(), data, "TZOFFSETFROM", "offset from")
})
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +0,0 @@
#!/bin/sh
set -eo pipefail
if ! echo "$PWD" | grep -q '/tzdata$'; then
echo "Please run this script from the tzdata dir"
exit 1
fi
# TODO: Generate from https://www.iana.org/time-zones
if [ ! -d /tmp/corso-tzdata ]; then
git clone --depth 1 https://github.com/add2cal/timezones-ical-library.git /tmp/corso-tzdata
else
cd /tmp/corso-tzdata
git pull
cd -
fi
# Generate a huge go file with all the timezones
echo "package tzdata" >data.go
echo "" >>data.go
echo "var TZData = map[string]string{" >>data.go
find /tmp/corso-tzdata/ -name '*.ics' | while read -r f; do
tz=$(echo "$f" | sed 's|/tmp/corso-tzdata/api/||;s|\.ics$||')
echo "Processing $tz"
printf "\t\"%s\": \`" "$tz" >>data.go
cat "$f" | grep -Ev "(BEGIN:|END:|TZID:)" |
sed 's|`|\\`|g;s|\r||;s|TZID:/timezones-ical-library/|TZID:|' |
perl -pe 'chomp if eof' >>data.go
echo "\`," >>data.go
done
echo "}" >>data.go

View File

@ -59,15 +59,6 @@ const (
minEpochDurationUpperBound = 7 * 24 * time.Hour minEpochDurationUpperBound = 7 * 24 * time.Hour
) )
// allValidCompressors is the set of compression algorithms either currently
// being used or that were previously used. Use this during the config verify
// command to avoid spurious errors. We can revisit whether we want to update
// the config in those old repos at a later time.
var allValidCompressors = map[compression.Name]struct{}{
compression.Name(defaultCompressor): {},
compression.Name("s2-default"): {},
}
var ( var (
ErrSettingDefaultConfig = clues.New("setting default repo config values") ErrSettingDefaultConfig = clues.New("setting default repo config values")
ErrorRepoAlreadyExists = clues.New("repo already exists") ErrorRepoAlreadyExists = clues.New("repo already exists")
@ -777,7 +768,7 @@ func (w *conn) verifyDefaultPolicyConfigOptions(
ctx = clues.Add(ctx, "current_global_policy", globalPol.String()) ctx = clues.Add(ctx, "current_global_policy", globalPol.String())
if _, ok := allValidCompressors[globalPol.CompressionPolicy.CompressorName]; !ok { if globalPol.CompressionPolicy.CompressorName != defaultCompressor {
errs.AddAlert(ctx, fault.NewAlert( errs.AddAlert(ctx, fault.NewAlert(
"unexpected compressor", "unexpected compressor",
corsoWrapperAlertNamespace, corsoWrapperAlertNamespace,

View File

@ -891,20 +891,6 @@ func (suite *ConnRetentionIntegrationSuite) TestVerifyDefaultConfigOptions() {
}, },
expectAlerts: 1, expectAlerts: 1,
}, },
{
name: "OldValidCompressor",
setupRepo: func(ctx context.Context, t *testing.T, con *conn) {
pol, err := con.getGlobalPolicyOrEmpty(ctx)
require.NoError(t, err, clues.ToCore(err))
_, err = updateCompressionOnPolicy("s2-default", pol)
require.NoError(t, err, clues.ToCore(err))
err = con.writeGlobalPolicy(ctx, "test", pol)
require.NoError(t, err, clues.ToCore(err))
},
expectAlerts: 0,
},
{ {
name: "NonDefaultCompression", name: "NonDefaultCompression",
setupRepo: func(ctx context.Context, t *testing.T, con *conn) { setupRepo: func(ctx context.Context, t *testing.T, con *conn) {

View File

@ -2,7 +2,6 @@ package m365
import ( import (
"context" "context"
"fmt"
"github.com/alcionai/clues" "github.com/alcionai/clues"
@ -110,7 +109,7 @@ func (ctrl *Controller) ProduceBackupCollections(
handler = teamschats.NewBackup() handler = teamschats.NewBackup()
default: default:
return nil, nil, false, clues.NewWC(ctx, fmt.Sprintf("service not supported: %s", service.HumanString())) return nil, nil, false, clues.Wrap(clues.NewWC(ctx, service.String()), "service not supported")
} }
colls, excludeItems, canUsePreviousBackup, err = handler.ProduceBackupCollections( colls, excludeItems, canUsePreviousBackup, err = handler.ProduceBackupCollections(
@ -174,8 +173,7 @@ func verifyBackupInputs(sel selectors.Selector, cachedIDs []string) error {
} }
if !filters.Contains(ids).Compare(sel.ID()) { if !filters.Contains(ids).Compare(sel.ID()) {
return clues.Wrap(core.ErrNotFound, "verifying existence of resource"). return clues.Stack(core.ErrNotFound).With("selector_protected_resource", sel.ID())
With("selector_protected_resource", sel.ID())
} }
return nil return nil

View File

@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
inMock "github.com/alcionai/corso/src/internal/common/idname/mock" inMock "github.com/alcionai/corso/src/internal/common/idname/mock"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/data/mock" "github.com/alcionai/corso/src/internal/data/mock"
"github.com/alcionai/corso/src/internal/m365/service/exchange" "github.com/alcionai/corso/src/internal/m365/service/exchange"
@ -18,7 +19,6 @@ import (
"github.com/alcionai/corso/src/internal/m365/service/sharepoint" "github.com/alcionai/corso/src/internal/m365/service/sharepoint"
"github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/internal/operations/inject"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/internal/version" "github.com/alcionai/corso/src/internal/version"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
@ -36,7 +36,10 @@ import (
type DataCollectionIntgSuite struct { type DataCollectionIntgSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup user string
site string
tenantID string
ac api.Client
} }
func TestDataCollectionIntgSuite(t *testing.T) { func TestDataCollectionIntgSuite(t *testing.T) {
@ -48,14 +51,29 @@ func TestDataCollectionIntgSuite(t *testing.T) {
} }
func (suite *DataCollectionIntgSuite) SetupSuite() { func (suite *DataCollectionIntgSuite) SetupSuite() {
suite.m365 = its.GetM365(suite.T()) t := suite.T()
suite.user = tconfig.M365UserID(t)
suite.site = tconfig.M365SiteID(t)
acct := tconfig.NewM365Account(t)
creds, err := acct.M365Config()
require.NoError(t, err, clues.ToCore(err))
suite.tenantID = creds.AzureTenantID
suite.ac, err = api.NewClient(
creds,
control.DefaultOptions(),
count.New())
require.NoError(t, err, clues.ToCore(err))
} }
func (suite *DataCollectionIntgSuite) TestExchangeDataCollection() { func (suite *DataCollectionIntgSuite) TestExchangeDataCollection() {
ctx, flush := tester.NewContext(suite.T()) ctx, flush := tester.NewContext(suite.T())
defer flush() defer flush()
selUsers := []string{suite.m365.User.ID} selUsers := []string{suite.user}
ctrl := newController(ctx, suite.T(), path.ExchangeService) ctrl := newController(ctx, suite.T(), path.ExchangeService)
tests := []struct { tests := []struct {
@ -67,7 +85,7 @@ func (suite *DataCollectionIntgSuite) TestExchangeDataCollection() {
getSelector: func(t *testing.T) selectors.Selector { getSelector: func(t *testing.T) selectors.Selector {
sel := selectors.NewExchangeBackup(selUsers) sel := selectors.NewExchangeBackup(selUsers)
sel.Include(sel.MailFolders([]string{api.MailInbox}, selectors.PrefixMatch())) sel.Include(sel.MailFolders([]string{api.MailInbox}, selectors.PrefixMatch()))
sel.DiscreteOwner = suite.m365.User.ID sel.DiscreteOwner = suite.user
return sel.Selector return sel.Selector
}, },
}, },
@ -76,7 +94,7 @@ func (suite *DataCollectionIntgSuite) TestExchangeDataCollection() {
getSelector: func(t *testing.T) selectors.Selector { getSelector: func(t *testing.T) selectors.Selector {
sel := selectors.NewExchangeBackup(selUsers) sel := selectors.NewExchangeBackup(selUsers)
sel.Include(sel.ContactFolders([]string{api.DefaultContacts}, selectors.PrefixMatch())) sel.Include(sel.ContactFolders([]string{api.DefaultContacts}, selectors.PrefixMatch()))
sel.DiscreteOwner = suite.m365.User.ID sel.DiscreteOwner = suite.user
return sel.Selector return sel.Selector
}, },
}, },
@ -124,8 +142,8 @@ func (suite *DataCollectionIntgSuite) TestExchangeDataCollection() {
collections, excludes, canUsePreviousBackup, err := exchange.NewBackup().ProduceBackupCollections( collections, excludes, canUsePreviousBackup, err := exchange.NewBackup().ProduceBackupCollections(
ctx, ctx,
bpc, bpc,
suite.m365.AC, suite.ac,
suite.m365.Creds, suite.ac.Credentials,
ctrl.UpdateStatus, ctrl.UpdateStatus,
count.New(), count.New(),
fault.New(true)) fault.New(true))
@ -252,7 +270,7 @@ func (suite *DataCollectionIntgSuite) TestSharePointDataCollection() {
ctx, flush := tester.NewContext(suite.T()) ctx, flush := tester.NewContext(suite.T())
defer flush() defer flush()
selSites := []string{suite.m365.Site.ID} selSites := []string{suite.site}
ctrl := newController(ctx, suite.T(), path.SharePointService) ctrl := newController(ctx, suite.T(), path.SharePointService)
tests := []struct { tests := []struct {
name string name string
@ -294,7 +312,7 @@ func (suite *DataCollectionIntgSuite) TestSharePointDataCollection() {
collections, excludes, canUsePreviousBackup, err := sharepoint.NewBackup().ProduceBackupCollections( collections, excludes, canUsePreviousBackup, err := sharepoint.NewBackup().ProduceBackupCollections(
ctx, ctx,
bpc, bpc,
suite.m365.AC, suite.ac,
ctrl.credentials, ctrl.credentials,
ctrl.UpdateStatus, ctrl.UpdateStatus,
count.New(), count.New(),
@ -333,7 +351,8 @@ func (suite *DataCollectionIntgSuite) TestSharePointDataCollection() {
type SPCollectionIntgSuite struct { type SPCollectionIntgSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup connector *Controller
user string
} }
func TestSPCollectionIntgSuite(t *testing.T) { func TestSPCollectionIntgSuite(t *testing.T) {
@ -345,7 +364,13 @@ func TestSPCollectionIntgSuite(t *testing.T) {
} }
func (suite *SPCollectionIntgSuite) SetupSuite() { func (suite *SPCollectionIntgSuite) SetupSuite() {
suite.m365 = its.GetM365(suite.T()) ctx, flush := tester.NewContext(suite.T())
defer flush()
suite.connector = newController(ctx, suite.T(), path.SharePointService)
suite.user = tconfig.M365UserID(suite.T())
tester.LogTimeOfTest(suite.T())
} }
func (suite *SPCollectionIntgSuite) TestCreateSharePointCollection_Libraries() { func (suite *SPCollectionIntgSuite) TestCreateSharePointCollection_Libraries() {
@ -354,20 +379,25 @@ func (suite *SPCollectionIntgSuite) TestCreateSharePointCollection_Libraries() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
ctrl := newController(ctx, t, path.SharePointService) var (
siteID = tconfig.M365SiteID(t)
ctrl = newController(ctx, t, path.SharePointService)
siteIDs = []string{siteID}
)
_, err := ctrl.PopulateProtectedResourceIDAndName(ctx, suite.m365.Site.ID, nil) site, err := ctrl.PopulateProtectedResourceIDAndName(ctx, siteID, nil)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
sel := selectors.NewSharePointBackup([]string{suite.m365.Site.ID}) sel := selectors.NewSharePointBackup(siteIDs)
sel.Include(sel.LibraryFolders([]string{"foo"}, selectors.PrefixMatch())) sel.Include(sel.LibraryFolders([]string{"foo"}, selectors.PrefixMatch()))
sel.Include(sel.Library("Documents")) sel.Include(sel.Library("Documents"))
sel.SetDiscreteOwnerIDName(suite.m365.Site.ID, suite.m365.Site.WebURL)
sel.SetDiscreteOwnerIDName(site.ID(), site.Name())
bpc := inject.BackupProducerConfig{ bpc := inject.BackupProducerConfig{
LastBackupVersion: version.NoBackup, LastBackupVersion: version.NoBackup,
Options: control.DefaultOptions(), Options: control.DefaultOptions(),
ProtectedResource: suite.m365.Site.Provider, ProtectedResource: site,
Selector: sel.Selector, Selector: sel.Selector,
} }
@ -385,15 +415,15 @@ func (suite *SPCollectionIntgSuite) TestCreateSharePointCollection_Libraries() {
) )
documentsColl, err := path.BuildPrefix( documentsColl, err := path.BuildPrefix(
suite.m365.TenantID, suite.connector.tenant,
suite.m365.Site.ID, siteID,
path.SharePointService, path.SharePointService,
path.LibrariesCategory) path.LibrariesCategory)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
metadataColl, err := path.BuildMetadata( metadataColl, err := path.BuildMetadata(
suite.m365.TenantID, suite.connector.tenant,
suite.m365.Site.ID, siteID,
path.SharePointService, path.SharePointService,
path.LibrariesCategory, path.LibrariesCategory,
false) false)
@ -420,19 +450,24 @@ func (suite *SPCollectionIntgSuite) TestCreateSharePointCollection_Lists() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
ctrl := newController(ctx, t, path.SharePointService) var (
siteID = tconfig.M365SiteID(t)
ctrl = newController(ctx, t, path.SharePointService)
siteIDs = []string{siteID}
)
_, err := ctrl.PopulateProtectedResourceIDAndName(ctx, suite.m365.Site.ID, nil) site, err := ctrl.PopulateProtectedResourceIDAndName(ctx, siteID, nil)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
sel := selectors.NewSharePointBackup([]string{suite.m365.Site.ID}) sel := selectors.NewSharePointBackup(siteIDs)
sel.Include(sel.Lists(selectors.Any())) sel.Include(sel.Lists(selectors.Any()))
sel.SetDiscreteOwnerIDName(suite.m365.Site.ID, suite.m365.Site.WebURL)
sel.SetDiscreteOwnerIDName(site.ID(), site.Name())
bpc := inject.BackupProducerConfig{ bpc := inject.BackupProducerConfig{
LastBackupVersion: version.NoBackup, LastBackupVersion: version.NoBackup,
Options: control.DefaultOptions(), Options: control.DefaultOptions(),
ProtectedResource: suite.m365.Site.Provider, ProtectedResource: site,
Selector: sel.Selector, Selector: sel.Selector,
} }
@ -467,7 +502,9 @@ func (suite *SPCollectionIntgSuite) TestCreateSharePointCollection_Lists() {
type GroupsCollectionIntgSuite struct { type GroupsCollectionIntgSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup connector *Controller
tenantID string
user string
} }
func TestGroupsCollectionIntgSuite(t *testing.T) { func TestGroupsCollectionIntgSuite(t *testing.T) {
@ -479,7 +516,21 @@ func TestGroupsCollectionIntgSuite(t *testing.T) {
} }
func (suite *GroupsCollectionIntgSuite) SetupSuite() { func (suite *GroupsCollectionIntgSuite) SetupSuite() {
suite.m365 = its.GetM365(suite.T()) t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
suite.connector = newController(ctx, t, path.GroupsService)
suite.user = tconfig.M365UserID(t)
acct := tconfig.NewM365Account(t)
creds, err := acct.M365Config()
require.NoError(t, err, clues.ToCore(err))
suite.tenantID = creds.AzureTenantID
tester.LogTimeOfTest(t)
} }
func (suite *GroupsCollectionIntgSuite) TestCreateGroupsCollection_SharePoint() { func (suite *GroupsCollectionIntgSuite) TestCreateGroupsCollection_SharePoint() {
@ -488,19 +539,24 @@ func (suite *GroupsCollectionIntgSuite) TestCreateGroupsCollection_SharePoint()
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
ctrl := newController(ctx, t, path.GroupsService) var (
groupID = tconfig.M365TeamID(t)
ctrl = newController(ctx, t, path.GroupsService)
groupIDs = []string{groupID}
)
_, err := ctrl.PopulateProtectedResourceIDAndName(ctx, suite.m365.Group.ID, nil) group, err := ctrl.PopulateProtectedResourceIDAndName(ctx, groupID, nil)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
sel := selectors.NewGroupsBackup([]string{suite.m365.Group.ID}) sel := selectors.NewGroupsBackup(groupIDs)
sel.Include(sel.LibraryFolders([]string{"test"}, selectors.PrefixMatch())) sel.Include(sel.LibraryFolders([]string{"test"}, selectors.PrefixMatch()))
sel.SetDiscreteOwnerIDName(suite.m365.Group.ID, suite.m365.Group.DisplayName)
sel.SetDiscreteOwnerIDName(group.ID(), group.Name())
bpc := inject.BackupProducerConfig{ bpc := inject.BackupProducerConfig{
LastBackupVersion: version.NoBackup, LastBackupVersion: version.NoBackup,
Options: control.DefaultOptions(), Options: control.DefaultOptions(),
ProtectedResource: suite.m365.Group.Provider, ProtectedResource: group,
Selector: sel.Selector, Selector: sel.Selector,
} }
@ -519,8 +575,8 @@ func (suite *GroupsCollectionIntgSuite) TestCreateGroupsCollection_SharePoint()
assert.Greater(t, len(collections), 1) assert.Greater(t, len(collections), 1)
p, err := path.BuildMetadata( p, err := path.BuildMetadata(
suite.m365.TenantID, suite.tenantID,
suite.m365.Group.ID, groupID,
path.GroupsService, path.GroupsService,
path.LibrariesCategory, path.LibrariesCategory,
false) false)
@ -558,23 +614,31 @@ func (suite *GroupsCollectionIntgSuite) TestCreateGroupsCollection_SharePoint_In
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
ctrl := newController(ctx, t, path.GroupsService) var (
groupID = tconfig.M365TeamID(t)
ctrl = newController(ctx, t, path.GroupsService)
groupIDs = []string{groupID}
)
_, err := ctrl.PopulateProtectedResourceIDAndName(ctx, suite.m365.Group.ID, nil) group, err := ctrl.PopulateProtectedResourceIDAndName(ctx, groupID, nil)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
sel := selectors.NewGroupsBackup([]string{suite.m365.Group.ID}) sel := selectors.NewGroupsBackup(groupIDs)
sel.Include(sel.LibraryFolders([]string{"test"}, selectors.PrefixMatch())) sel.Include(sel.LibraryFolders([]string{"test"}, selectors.PrefixMatch()))
sel.SetDiscreteOwnerIDName(suite.m365.Group.ID, suite.m365.Group.DisplayName)
sel.SetDiscreteOwnerIDName(group.ID(), group.Name())
site, err := suite.connector.AC.Groups().GetRootSite(ctx, groupID)
require.NoError(t, err, clues.ToCore(err))
pth, err := path.Build( pth, err := path.Build(
suite.m365.TenantID, suite.tenantID,
suite.m365.Group.ID, groupID,
path.GroupsService, path.GroupsService,
path.LibrariesCategory, path.LibrariesCategory,
true, true,
odConsts.SitesPathDir, odConsts.SitesPathDir,
suite.m365.Group.RootSite.ID) ptr.Val(site.GetId()))
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
mmc := []data.RestoreCollection{ mmc := []data.RestoreCollection{
@ -592,7 +656,7 @@ func (suite *GroupsCollectionIntgSuite) TestCreateGroupsCollection_SharePoint_In
bpc := inject.BackupProducerConfig{ bpc := inject.BackupProducerConfig{
LastBackupVersion: version.NoBackup, LastBackupVersion: version.NoBackup,
Options: control.DefaultOptions(), Options: control.DefaultOptions(),
ProtectedResource: suite.m365.Group.Provider, ProtectedResource: group,
Selector: sel.Selector, Selector: sel.Selector,
MetadataCollections: mmc, MetadataCollections: mmc,
} }
@ -612,8 +676,8 @@ func (suite *GroupsCollectionIntgSuite) TestCreateGroupsCollection_SharePoint_In
assert.Greater(t, len(collections), 1) assert.Greater(t, len(collections), 1)
p, err := path.BuildMetadata( p, err := path.BuildMetadata(
suite.m365.TenantID, suite.tenantID,
suite.m365.Group.ID, groupID,
path.GroupsService, path.GroupsService,
path.LibrariesCategory, path.LibrariesCategory,
false) false)
@ -626,13 +690,13 @@ func (suite *GroupsCollectionIntgSuite) TestCreateGroupsCollection_SharePoint_In
foundRootTombstone := false foundRootTombstone := false
sp, err := path.BuildPrefix( sp, err := path.BuildPrefix(
suite.m365.TenantID, suite.tenantID,
suite.m365.Group.ID, groupID,
path.GroupsService, path.GroupsService,
path.LibrariesCategory) path.LibrariesCategory)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
sp, err = sp.Append(false, odConsts.SitesPathDir, suite.m365.Group.RootSite.ID) sp, err = sp.Append(false, odConsts.SitesPathDir, ptr.Val(site.GetId()))
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
for _, coll := range collections { for _, coll := range collections {

View File

@ -366,7 +366,7 @@ func downloadContent(
itemID := ptr.Val(item.GetId()) itemID := ptr.Val(item.GetId())
ctx = clues.Add(ctx, "item_id", itemID) ctx = clues.Add(ctx, "item_id", itemID)
content, err := downloadItem(ctx, iaag, driveID, item) content, err := downloadItem(ctx, iaag, item)
if err == nil { if err == nil {
return content, nil return content, nil
} else if !graph.IsErrUnauthorizedOrBadToken(err) { } else if !graph.IsErrUnauthorizedOrBadToken(err) {
@ -395,7 +395,7 @@ func downloadContent(
cdi := custom.ToCustomDriveItem(di) cdi := custom.ToCustomDriveItem(di)
content, err = downloadItem(ctx, iaag, driveID, cdi) content, err = downloadItem(ctx, iaag, cdi)
if err != nil { if err != nil {
return nil, clues.Wrap(err, "content download retry") return nil, clues.Wrap(err, "content download retry")
} }
@ -426,7 +426,7 @@ func readItemContents(
return nil, core.ErrNotFound return nil, core.ErrNotFound
} }
rc, err := downloadFile(ctx, iaag, props.downloadURL, false) rc, err := downloadFile(ctx, iaag, props.downloadURL)
if graph.IsErrUnauthorizedOrBadToken(err) { if graph.IsErrUnauthorizedOrBadToken(err) {
logger.CtxErr(ctx, err).Debug("stale item in cache") logger.CtxErr(ctx, err).Debug("stale item in cache")
} }

View File

@ -22,6 +22,8 @@ import (
odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts" odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts"
"github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/m365/support"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
bupMD "github.com/alcionai/corso/src/pkg/backup/metadata" bupMD "github.com/alcionai/corso/src/pkg/backup/metadata"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
@ -39,6 +41,50 @@ import (
const defaultFileSize int64 = 42 const defaultFileSize int64 = 42
// TODO(ashmrtn): Merge with similar structs in graph and exchange packages.
type oneDriveService struct {
credentials account.M365Config
status support.ControllerOperationStatus
ac api.Client
}
func newOneDriveService(credentials account.M365Config) (*oneDriveService, error) {
ac, err := api.NewClient(
credentials,
control.DefaultOptions(),
count.New())
if err != nil {
return nil, err
}
service := oneDriveService{
ac: ac,
credentials: credentials,
}
return &service, nil
}
func (ods *oneDriveService) updateStatus(status *support.ControllerOperationStatus) {
if status == nil {
return
}
ods.status = support.MergeStatus(ods.status, *status)
}
func loadTestService(t *testing.T) *oneDriveService {
a := tconfig.NewM365Account(t)
creds, err := a.M365Config()
require.NoError(t, err, clues.ToCore(err))
service, err := newOneDriveService(creds)
require.NoError(t, err, clues.ToCore(err))
return service
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// collections // collections
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -795,12 +841,7 @@ func (h mockBackupHandler[T]) AugmentItemInfo(
return h.ItemInfo return h.ItemInfo
} }
func (h *mockBackupHandler[T]) Get( func (h *mockBackupHandler[T]) Get(context.Context, string, map[string]string) (*http.Response, error) {
context.Context,
string,
map[string]string,
bool,
) (*http.Response, error) {
c := h.getCall c := h.getCall
h.getCall++ h.getCall++

View File

@ -23,8 +23,6 @@ import (
const ( const (
acceptHeaderKey = "Accept" acceptHeaderKey = "Accept"
acceptHeaderValue = "*/*" acceptHeaderValue = "*/*"
gigabyte = 1024 * 1024 * 1024
largeFileDownloadLimit = 15 * gigabyte
) )
// downloadUrlKeys is used to find the download URL in a DriveItem response. // downloadUrlKeys is used to find the download URL in a DriveItem response.
@ -35,8 +33,7 @@ var downloadURLKeys = []string{
func downloadItem( func downloadItem(
ctx context.Context, ctx context.Context,
getter api.Getter, ag api.Getter,
driveID string,
item *custom.DriveItem, item *custom.DriveItem,
) (io.ReadCloser, error) { ) (io.ReadCloser, error) {
if item == nil { if item == nil {
@ -44,36 +41,35 @@ func downloadItem(
} }
var ( var (
// very large file content needs to be downloaded through a different endpoint, or else rc io.ReadCloser
// the download could take longer than the lifespan of the download token in the cached isFile = item.GetFile() != nil
// url, which will cause us to timeout on every download request, even if we refresh the
// download url right before the query.
url = "https://graph.microsoft.com/v1.0/drives/" + driveID + "/items/" + ptr.Val(item.GetId()) + "/content"
reader io.ReadCloser
err error err error
isLargeFile = ptr.Val(item.GetSize()) > largeFileDownloadLimit
) )
// if this isn't a file, no content is available for download if isFile {
if item.GetFile() == nil { var (
return reader, nil url string
ad = item.GetAdditionalData()
)
for _, key := range downloadURLKeys {
if v, err := str.AnyValueToString(key, ad); err == nil {
url = v
break
}
} }
// smaller files will maintain our current behavior (prefetching the download url with the rc, err = downloadFile(ctx, ag, url)
// url cache). That pattern works for us in general, and we only need to deviate for very if err != nil {
// large file sizes. return nil, clues.Stack(err)
if !isLargeFile { }
url = str.FirstIn(item.GetAdditionalData(), downloadURLKeys...)
} }
reader, err = downloadFile(ctx, getter, url, isLargeFile) return rc, nil
return reader, clues.StackWC(ctx, err).OrNil()
} }
type downloadWithRetries struct { type downloadWithRetries struct {
getter api.Getter getter api.Getter
requireAuth bool
url string url string
} }
@ -90,7 +86,7 @@ func (dg *downloadWithRetries) Get(
// wouldn't work without it (get 416 responses instead of 206). // wouldn't work without it (get 416 responses instead of 206).
headers[acceptHeaderKey] = acceptHeaderValue headers[acceptHeaderKey] = acceptHeaderValue
resp, err := dg.getter.Get(ctx, dg.url, headers, dg.requireAuth) resp, err := dg.getter.Get(ctx, dg.url, headers)
if err != nil { if err != nil {
return nil, clues.Wrap(err, "getting file") return nil, clues.Wrap(err, "getting file")
} }
@ -100,7 +96,7 @@ func (dg *downloadWithRetries) Get(
resp.Body.Close() resp.Body.Close()
} }
return nil, clues.NewWC(ctx, "malware detected").Label(graph.LabelsMalware) return nil, clues.New("malware detected").Label(graph.LabelsMalware)
} }
if resp != nil && (resp.StatusCode/100) != 2 { if resp != nil && (resp.StatusCode/100) != 2 {
@ -111,7 +107,7 @@ func (dg *downloadWithRetries) Get(
// upstream error checks can compare the status with // upstream error checks can compare the status with
// clues.HasLabel(err, graph.LabelStatus(http.KnownStatusCode)) // clues.HasLabel(err, graph.LabelStatus(http.KnownStatusCode))
return nil, clues. return nil, clues.
Wrap(clues.NewWC(ctx, resp.Status), "non-2xx http response"). Wrap(clues.New(resp.Status), "non-2xx http response").
Label(graph.LabelStatus(resp.StatusCode)) Label(graph.LabelStatus(resp.StatusCode))
} }
@ -122,7 +118,6 @@ func downloadFile(
ctx context.Context, ctx context.Context,
ag api.Getter, ag api.Getter,
url string, url string,
requireAuth bool,
) (io.ReadCloser, error) { ) (io.ReadCloser, error) {
if len(url) == 0 { if len(url) == 0 {
return nil, clues.NewWC(ctx, "empty file url") return nil, clues.NewWC(ctx, "empty file url")
@ -147,7 +142,6 @@ func downloadFile(
ctx, ctx,
&downloadWithRetries{ &downloadWithRetries{
getter: ag, getter: ag,
requireAuth: requireAuth,
url: url, url: url,
}) })

View File

@ -12,7 +12,6 @@ import (
"github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/internal/common/prefixmatcher" "github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/m365/support"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
@ -154,7 +153,6 @@ func (suite *ItemCollectorUnitSuite) TestDrives() {
{ {
Values: nil, Values: nil,
NextLink: nil, NextLink: nil,
// needs graph.Stack, not clues.Stack
Err: graph.Stack(ctx, mySiteURLNotFound), Err: graph.Stack(ctx, mySiteURLNotFound),
}, },
}, },
@ -167,7 +165,6 @@ func (suite *ItemCollectorUnitSuite) TestDrives() {
{ {
Values: nil, Values: nil,
NextLink: nil, NextLink: nil,
// needs graph.Stack, not clues.Stack
Err: graph.Stack(ctx, mySiteNotFound), Err: graph.Stack(ctx, mySiteNotFound),
}, },
}, },
@ -234,18 +231,6 @@ func (suite *OneDriveIntgSuite) SetupSuite() {
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
} }
type stubStatusUpdater struct {
status support.ControllerOperationStatus
}
func (ssu *stubStatusUpdater) updateStatus(status *support.ControllerOperationStatus) {
if status == nil {
return
}
ssu.status = support.MergeStatus(ssu.status, *status)
}
func (suite *OneDriveIntgSuite) TestOneDriveNewCollections() { func (suite *OneDriveIntgSuite) TestOneDriveNewCollections() {
creds, err := tconfig.NewM365Account(suite.T()).M365Config() creds, err := tconfig.NewM365Account(suite.T()).M365Config()
require.NoError(suite.T(), err, clues.ToCore(err)) require.NoError(suite.T(), err, clues.ToCore(err))
@ -271,10 +256,10 @@ func (suite *OneDriveIntgSuite) TestOneDriveNewCollections() {
defer flush() defer flush()
var ( var (
service = loadTestService(t)
scope = selectors. scope = selectors.
NewOneDriveBackup([]string{test.user}). NewOneDriveBackup([]string{test.user}).
AllData()[0] AllData()[0]
statusUpdater = stubStatusUpdater{}
) )
colls := NewCollections( colls := NewCollections(
@ -287,7 +272,7 @@ func (suite *OneDriveIntgSuite) TestOneDriveNewCollections() {
}, },
creds.AzureTenantID, creds.AzureTenantID,
idname.NewProvider(test.user, test.user), idname.NewProvider(test.user, test.user),
statusUpdater.updateStatus, service.updateStatus,
control.Options{ control.Options{
ToggleFeatures: control.Toggles{}, ToggleFeatures: control.Toggles{},
}, },

View File

@ -17,7 +17,6 @@ import (
"github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/common/str" "github.com/alcionai/corso/src/internal/common/str"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/control/testdata" "github.com/alcionai/corso/src/pkg/control/testdata"
@ -31,7 +30,9 @@ import (
type ItemIntegrationSuite struct { type ItemIntegrationSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup user string
userDriveID string
service *oneDriveService
} }
func TestItemIntegrationSuite(t *testing.T) { func TestItemIntegrationSuite(t *testing.T) {
@ -43,7 +44,25 @@ func TestItemIntegrationSuite(t *testing.T) {
} }
func (suite *ItemIntegrationSuite) SetupSuite() { func (suite *ItemIntegrationSuite) SetupSuite() {
suite.m365 = its.GetM365(suite.T()) t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
suite.service = loadTestService(t)
suite.user = tconfig.SecondaryM365UserID(t)
graph.InitializeConcurrencyLimiter(ctx, true, 4)
pager := suite.service.ac.Drives().NewUserDrivePager(suite.user, nil)
odDrives, err := api.GetAllDrives(ctx, pager)
require.NoError(t, err, clues.ToCore(err))
// Test Requirement 1: Need a drive
require.Greaterf(t, len(odDrives), 0, "user %s does not have a drive", suite.user)
// Pick the first drive
suite.userDriveID = ptr.Val(odDrives[0].GetId())
} }
func getOneDriveItem( func getOneDriveItem(
@ -84,36 +103,28 @@ func (suite *ItemIntegrationSuite) TestItemReader_oneDrive() {
defer flush() defer flush()
sc := selectors. sc := selectors.
NewOneDriveBackup([]string{suite.m365.User.ID}). NewOneDriveBackup([]string{suite.user}).
AllData()[0] AllData()[0]
driveItem := getOneDriveItem( driveItem := getOneDriveItem(ctx, t, suite.service.ac, suite.userDriveID)
ctx,
t,
suite.m365.AC,
suite.m365.User.DriveID)
// Test Requirement 2: Need a file // Test Requirement 2: Need a file
require.NotEmpty( require.NotEmpty(
t, t,
driveItem, driveItem,
"no file item found for user %q drive %q", "no file item found for user %s drive %s",
suite.m365.User.ID, suite.user,
suite.m365.User.DriveID) suite.userDriveID)
bh := &userDriveBackupHandler{ bh := &userDriveBackupHandler{
baseUserDriveHandler: baseUserDriveHandler{ baseUserDriveHandler: baseUserDriveHandler{
ac: suite.m365.AC.Drives(), ac: suite.service.ac.Drives(),
}, },
userID: suite.m365.User.ID, userID: suite.user,
scope: sc, scope: sc,
} }
// Read data for the file // Read data for the file
itemData, err := downloadItem( itemData, err := downloadItem(ctx, bh, custom.ToCustomDriveItem(driveItem))
ctx,
bh,
suite.m365.User.DriveID,
custom.ToCustomDriveItem(driveItem))
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
size, err := io.Copy(io.Discard, itemData) size, err := io.Copy(io.Discard, itemData)
@ -131,13 +142,13 @@ func (suite *ItemIntegrationSuite) TestIsURLExpired() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
driveItem := getOneDriveItem(ctx, t, suite.m365.AC, suite.m365.User.DriveID) driveItem := getOneDriveItem(ctx, t, suite.service.ac, suite.userDriveID)
require.NotEmpty( require.NotEmpty(
t, t,
driveItem, driveItem,
"no file item found for user %q drive %q", "no file item found for user %s drive %s",
suite.m365.User.ID, suite.user,
suite.m365.User.DriveID) suite.userDriveID)
var url string var url string
@ -162,7 +173,7 @@ func (suite *ItemIntegrationSuite) TestItemWriter() {
}{ }{
{ {
name: "", name: "",
driveID: suite.m365.User.DriveID, driveID: suite.userDriveID,
}, },
// { // {
// name: "sharePoint", // name: "sharePoint",
@ -172,12 +183,12 @@ func (suite *ItemIntegrationSuite) TestItemWriter() {
for _, test := range table { for _, test := range table {
suite.Run(test.name, func() { suite.Run(test.name, func() {
t := suite.T() t := suite.T()
rh := NewUserDriveRestoreHandler(suite.m365.AC) rh := NewUserDriveRestoreHandler(suite.service.ac)
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
root, err := suite.m365.AC.Drives().GetRootFolder(ctx, test.driveID) root, err := suite.service.ac.Drives().GetRootFolder(ctx, test.driveID)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
newFolderName := testdata.DefaultRestoreConfig("folder").Location newFolderName := testdata.DefaultRestoreConfig("folder").Location
@ -206,7 +217,7 @@ func (suite *ItemIntegrationSuite) TestItemWriter() {
// HACK: Leveraging this to test getFolder behavior for a file. `getFolder()` on the // HACK: Leveraging this to test getFolder behavior for a file. `getFolder()` on the
// newly created item should fail because it's a file not a folder // newly created item should fail because it's a file not a folder
_, err = suite.m365.AC.Drives().GetFolderByName( _, err = suite.service.ac.Drives().GetFolderByName(
ctx, ctx,
test.driveID, test.driveID,
ptr.Val(newFolder.GetId()), ptr.Val(newFolder.GetId()),
@ -250,7 +261,7 @@ func (suite *ItemIntegrationSuite) TestDriveGetFolder() {
}{ }{
{ {
name: "oneDrive", name: "oneDrive",
driveID: suite.m365.User.DriveID, driveID: suite.userDriveID,
}, },
// { // {
// name: "sharePoint", // name: "sharePoint",
@ -264,11 +275,11 @@ func (suite *ItemIntegrationSuite) TestDriveGetFolder() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
root, err := suite.m365.AC.Drives().GetRootFolder(ctx, test.driveID) root, err := suite.service.ac.Drives().GetRootFolder(ctx, test.driveID)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
// Lookup a folder that doesn't exist // Lookup a folder that doesn't exist
_, err = suite.m365.AC.Drives().GetFolderByName( _, err = suite.service.ac.Drives().GetFolderByName(
ctx, ctx,
test.driveID, test.driveID,
ptr.Val(root.GetId()), ptr.Val(root.GetId()),
@ -276,7 +287,7 @@ func (suite *ItemIntegrationSuite) TestDriveGetFolder() {
require.ErrorIs(t, err, api.ErrFolderNotFound, clues.ToCore(err)) require.ErrorIs(t, err, api.ErrFolderNotFound, clues.ToCore(err))
// Lookup a folder that does exist // Lookup a folder that does exist
_, err = suite.m365.AC.Drives().GetFolderByName( _, err = suite.service.ac.Drives().GetFolderByName(
ctx, ctx,
test.driveID, test.driveID,
ptr.Val(root.GetId()), ptr.Val(root.GetId()),
@ -296,7 +307,6 @@ func (m mockGetter) Get(
ctx context.Context, ctx context.Context,
url string, url string,
headers map[string]string, headers map[string]string,
requireAuth bool,
) (*http.Response, error) { ) (*http.Response, error) {
return m.GetFunc(ctx, url) return m.GetFunc(ctx, url)
} }
@ -384,7 +394,7 @@ func (suite *ItemUnitTestSuite) TestDownloadItem() {
return nil, clues.New("test error") return nil, clues.New("test error")
}, },
errorExpected: require.Error, errorExpected: require.Error,
rcExpected: require.NotNil, rcExpected: require.Nil,
}, },
{ {
name: "download url is empty", name: "download url is empty",
@ -421,7 +431,7 @@ func (suite *ItemUnitTestSuite) TestDownloadItem() {
}, nil }, nil
}, },
errorExpected: require.Error, errorExpected: require.Error,
rcExpected: require.NotNil, rcExpected: require.Nil,
}, },
{ {
name: "non-2xx http response", name: "non-2xx http response",
@ -440,7 +450,7 @@ func (suite *ItemUnitTestSuite) TestDownloadItem() {
}, nil }, nil
}, },
errorExpected: require.Error, errorExpected: require.Error,
rcExpected: require.NotNil, rcExpected: require.Nil,
}, },
} }
@ -453,78 +463,9 @@ func (suite *ItemUnitTestSuite) TestDownloadItem() {
mg := mockGetter{ mg := mockGetter{
GetFunc: test.GetFunc, GetFunc: test.GetFunc,
} }
rc, err := downloadItem( rc, err := downloadItem(ctx, mg, custom.ToCustomDriveItem(test.itemFunc()))
ctx,
mg,
"driveID",
custom.ToCustomDriveItem(test.itemFunc()))
test.errorExpected(t, err, clues.ToCore(err)) test.errorExpected(t, err, clues.ToCore(err))
test.rcExpected(t, rc, "reader should only be nil if item is nil") test.rcExpected(t, rc)
})
}
}
func (suite *ItemUnitTestSuite) TestDownloadItem_urlByFileSize() {
var (
testRc = io.NopCloser(bytes.NewReader([]byte("test")))
url = "https://example.com"
okResp = &http.Response{
StatusCode: http.StatusOK,
Body: testRc,
}
)
table := []struct {
name string
itemFunc func() models.DriveItemable
GetFunc func(ctx context.Context, url string) (*http.Response, error)
errorExpected require.ErrorAssertionFunc
rcExpected require.ValueAssertionFunc
label string
}{
{
name: "big file",
itemFunc: func() models.DriveItemable {
di := api.NewDriveItem("test", false)
di.SetAdditionalData(map[string]any{"@microsoft.graph.downloadUrl": url})
di.SetSize(ptr.To[int64](20 * gigabyte))
return di
},
GetFunc: func(ctx context.Context, url string) (*http.Response, error) {
assert.Contains(suite.T(), url, "/content")
return okResp, nil
},
},
{
name: "small file",
itemFunc: func() models.DriveItemable {
di := api.NewDriveItem("test", false)
di.SetAdditionalData(map[string]any{"@microsoft.graph.downloadUrl": url})
di.SetSize(ptr.To[int64](2 * gigabyte))
return di
},
GetFunc: func(ctx context.Context, url string) (*http.Response, error) {
assert.NotContains(suite.T(), url, "/content")
return okResp, nil
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
_, err := downloadItem(
ctx,
mockGetter{GetFunc: test.GetFunc},
"driveID",
custom.ToCustomDriveItem(test.itemFunc()))
require.NoError(t, err, clues.ToCore(err))
}) })
} }
} }
@ -581,11 +522,7 @@ func (suite *ItemUnitTestSuite) TestDownloadItem_ConnectionResetErrorOnFirstRead
mg := mockGetter{ mg := mockGetter{
GetFunc: GetFunc, GetFunc: GetFunc,
} }
rc, err := downloadItem( rc, err := downloadItem(ctx, mg, custom.ToCustomDriveItem(itemFunc()))
ctx,
mg,
"driveID",
custom.ToCustomDriveItem(itemFunc()))
errorExpected(t, err, clues.ToCore(err)) errorExpected(t, err, clues.ToCore(err))
rcExpected(t, rc) rcExpected(t, rc)

View File

@ -93,9 +93,8 @@ func (h siteBackupHandler) Get(
ctx context.Context, ctx context.Context,
url string, url string,
headers map[string]string, headers map[string]string,
requireAuth bool,
) (*http.Response, error) { ) (*http.Response, error) {
return h.ac.Get(ctx, url, headers, requireAuth) return h.ac.Get(ctx, url, headers)
} }
func (h siteBackupHandler) PathPrefix( func (h siteBackupHandler) PathPrefix(

View File

@ -18,7 +18,6 @@ import (
"github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/control/testdata" "github.com/alcionai/corso/src/pkg/control/testdata"
@ -35,7 +34,9 @@ import (
type URLCacheIntegrationSuite struct { type URLCacheIntegrationSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup ac api.Client
user string
driveID string
} }
func TestURLCacheIntegrationSuite(t *testing.T) { func TestURLCacheIntegrationSuite(t *testing.T) {
@ -48,12 +49,29 @@ func TestURLCacheIntegrationSuite(t *testing.T) {
func (suite *URLCacheIntegrationSuite) SetupSuite() { func (suite *URLCacheIntegrationSuite) SetupSuite() {
t := suite.T() t := suite.T()
suite.m365 = its.GetM365(t)
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
graph.InitializeConcurrencyLimiter(ctx, true, 4) graph.InitializeConcurrencyLimiter(ctx, true, 4)
suite.user = tconfig.SecondaryM365UserID(t)
acct := tconfig.NewM365Account(t)
creds, err := acct.M365Config()
require.NoError(t, err, clues.ToCore(err))
suite.ac, err = api.NewClient(
creds,
control.DefaultOptions(),
count.New())
require.NoError(t, err, clues.ToCore(err))
drive, err := suite.ac.Users().GetDefaultDrive(ctx, suite.user)
require.NoError(t, err, clues.ToCore(err))
suite.driveID = ptr.Val(drive.GetId())
} }
// Basic test for urlCache. Create some files in onedrive, then access them via // Basic test for urlCache. Create some files in onedrive, then access them via
@ -61,18 +79,22 @@ func (suite *URLCacheIntegrationSuite) SetupSuite() {
func (suite *URLCacheIntegrationSuite) TestURLCacheBasic() { func (suite *URLCacheIntegrationSuite) TestURLCacheBasic() {
var ( var (
t = suite.T() t = suite.T()
ac = suite.m365.AC.Drives() ac = suite.ac.Drives()
driveID = suite.m365.User.DriveID driveID = suite.driveID
newFolderName = testdata.DefaultRestoreConfig("folder").Location newFolderName = testdata.DefaultRestoreConfig("folder").Location
) )
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
// Create a new test folder
root, err := ac.GetRootFolder(ctx, driveID)
require.NoError(t, err, clues.ToCore(err))
newFolder, err := ac.PostItemInContainer( newFolder, err := ac.PostItemInContainer(
ctx, ctx,
driveID, driveID,
suite.m365.User.DriveRootFolderID, ptr.Val(root.GetId()),
api.NewDriveItem(newFolderName, true), api.NewDriveItem(newFolderName, true),
control.Copy) control.Copy)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
@ -83,7 +105,7 @@ func (suite *URLCacheIntegrationSuite) TestURLCacheBasic() {
// Get the previous delta to feed into url cache // Get the previous delta to feed into url cache
pager := ac.EnumerateDriveItemsDelta( pager := ac.EnumerateDriveItemsDelta(
ctx, ctx,
driveID, suite.driveID,
"", "",
api.CallConfig{ api.CallConfig{
Select: api.URLCacheDriveItemProps(), Select: api.URLCacheDriveItemProps(),
@ -120,10 +142,10 @@ func (suite *URLCacheIntegrationSuite) TestURLCacheBasic() {
// Create a new URL cache with a long TTL // Create a new URL cache with a long TTL
uc, err := newURLCache( uc, err := newURLCache(
driveID, suite.driveID,
du.URL, du.URL,
1*time.Hour, 1*time.Hour,
ac, suite.ac.Drives(),
count.New(), count.New(),
fault.New(true)) fault.New(true))
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
@ -154,8 +176,7 @@ func (suite *URLCacheIntegrationSuite) TestURLCacheBasic() {
http.MethodGet, http.MethodGet,
props.downloadURL, props.downloadURL,
nil, nil,
nil, nil)
false)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
require.NotNil(t, resp) require.NotNil(t, resp)

View File

@ -93,9 +93,8 @@ func (h userDriveBackupHandler) Get(
ctx context.Context, ctx context.Context,
url string, url string,
headers map[string]string, headers map[string]string,
requireAuth bool,
) (*http.Response, error) { ) (*http.Response, error) {
return h.ac.Get(ctx, url, headers, requireAuth) return h.ac.Get(ctx, url, headers)
} }
func (h userDriveBackupHandler) PathPrefix( func (h userDriveBackupHandler) PathPrefix(

View File

@ -296,7 +296,6 @@ func populateCollections(
cl), cl),
qp.ProtectedResource.ID(), qp.ProtectedResource.ID(),
bh.itemHandler(), bh.itemHandler(),
bh,
addAndRem.Added, addAndRem.Added,
addAndRem.Removed, addAndRem.Removed,
// TODO: produce a feature flag that allows selective // TODO: produce a feature flag that allows selective

View File

@ -24,7 +24,6 @@ import (
"github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/m365/support"
"github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/internal/operations/inject"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/internal/version" "github.com/alcionai/corso/src/internal/version"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
@ -88,14 +87,6 @@ func (bh mockBackupHandler) folderGetter() containerGetter { return
func (bh mockBackupHandler) previewIncludeContainers() []string { return bh.previewIncludes } func (bh mockBackupHandler) previewIncludeContainers() []string { return bh.previewIncludes }
func (bh mockBackupHandler) previewExcludeContainers() []string { return bh.previewExcludes } func (bh mockBackupHandler) previewExcludeContainers() []string { return bh.previewExcludes }
func (bh mockBackupHandler) CanSkipItemFailure(
err error,
resourceID string,
opts control.Options,
) (fault.SkipCause, bool) {
return "", false
}
func (bh mockBackupHandler) NewContainerCache( func (bh mockBackupHandler) NewContainerCache(
userID string, userID string,
) (string, graph.ContainerResolver) { ) (string, graph.ContainerResolver) {
@ -481,7 +472,10 @@ func newStatusUpdater(t *testing.T, wg *sync.WaitGroup) func(status *support.Con
type BackupIntgSuite struct { type BackupIntgSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup user string
site string
tenantID string
ac api.Client
} }
func TestBackupIntgSuite(t *testing.T) { func TestBackupIntgSuite(t *testing.T) {
@ -494,18 +488,35 @@ func TestBackupIntgSuite(t *testing.T) {
func (suite *BackupIntgSuite) SetupSuite() { func (suite *BackupIntgSuite) SetupSuite() {
t := suite.T() t := suite.T()
suite.m365 = its.GetM365(t)
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
graph.InitializeConcurrencyLimiter(ctx, true, 4) graph.InitializeConcurrencyLimiter(ctx, true, 4)
suite.user = tconfig.M365UserID(t)
suite.site = tconfig.M365SiteID(t)
acct := tconfig.NewM365Account(t)
creds, err := acct.M365Config()
require.NoError(t, err, clues.ToCore(err))
suite.ac, err = api.NewClient(
creds,
control.DefaultOptions(),
count.New())
require.NoError(t, err, clues.ToCore(err))
suite.tenantID = creds.AzureTenantID
tester.LogTimeOfTest(t)
} }
func (suite *BackupIntgSuite) TestMailFetch() { func (suite *BackupIntgSuite) TestMailFetch() {
var ( var (
users = []string{suite.m365.User.ID} userID = tconfig.M365UserID(suite.T())
handlers = BackupHandlers(suite.m365.AC) users = []string{userID}
handlers = BackupHandlers(suite.ac)
) )
tests := []struct { tests := []struct {
@ -549,14 +560,14 @@ func (suite *BackupIntgSuite) TestMailFetch() {
bpc := inject.BackupProducerConfig{ bpc := inject.BackupProducerConfig{
LastBackupVersion: version.NoBackup, LastBackupVersion: version.NoBackup,
Options: ctrlOpts, Options: ctrlOpts,
ProtectedResource: suite.m365.User.Provider, ProtectedResource: inMock.NewProvider(userID, userID),
} }
collections, err := CreateCollections( collections, err := CreateCollections(
ctx, ctx,
bpc, bpc,
handlers, handlers,
suite.m365.TenantID, suite.tenantID,
test.scope, test.scope,
metadata.DeltaPaths{}, metadata.DeltaPaths{},
func(status *support.ControllerOperationStatus) {}, func(status *support.ControllerOperationStatus) {},
@ -591,8 +602,9 @@ func (suite *BackupIntgSuite) TestMailFetch() {
func (suite *BackupIntgSuite) TestDelta() { func (suite *BackupIntgSuite) TestDelta() {
var ( var (
users = []string{suite.m365.User.ID} userID = tconfig.M365UserID(suite.T())
handlers = BackupHandlers(suite.m365.AC) users = []string{userID}
handlers = BackupHandlers(suite.ac)
) )
tests := []struct { tests := []struct {
@ -628,7 +640,7 @@ func (suite *BackupIntgSuite) TestDelta() {
bpc := inject.BackupProducerConfig{ bpc := inject.BackupProducerConfig{
LastBackupVersion: version.NoBackup, LastBackupVersion: version.NoBackup,
Options: control.DefaultOptions(), Options: control.DefaultOptions(),
ProtectedResource: suite.m365.User.Provider, ProtectedResource: inMock.NewProvider(userID, userID),
} }
// get collections without providing any delta history (ie: full backup) // get collections without providing any delta history (ie: full backup)
@ -636,7 +648,7 @@ func (suite *BackupIntgSuite) TestDelta() {
ctx, ctx,
bpc, bpc,
handlers, handlers,
suite.m365.TenantID, suite.tenantID,
test.scope, test.scope,
metadata.DeltaPaths{}, metadata.DeltaPaths{},
func(status *support.ControllerOperationStatus) {}, func(status *support.ControllerOperationStatus) {},
@ -669,7 +681,7 @@ func (suite *BackupIntgSuite) TestDelta() {
ctx, ctx,
bpc, bpc,
handlers, handlers,
suite.m365.TenantID, suite.tenantID,
test.scope, test.scope,
dps, dps,
func(status *support.ControllerOperationStatus) {}, func(status *support.ControllerOperationStatus) {},
@ -691,8 +703,8 @@ func (suite *BackupIntgSuite) TestMailSerializationRegression() {
var ( var (
wg sync.WaitGroup wg sync.WaitGroup
users = []string{suite.m365.User.ID} users = []string{suite.user}
handlers = BackupHandlers(suite.m365.AC) handlers = BackupHandlers(suite.ac)
) )
sel := selectors.NewExchangeBackup(users) sel := selectors.NewExchangeBackup(users)
@ -701,7 +713,7 @@ func (suite *BackupIntgSuite) TestMailSerializationRegression() {
bpc := inject.BackupProducerConfig{ bpc := inject.BackupProducerConfig{
LastBackupVersion: version.NoBackup, LastBackupVersion: version.NoBackup,
Options: control.DefaultOptions(), Options: control.DefaultOptions(),
ProtectedResource: suite.m365.User.Provider, ProtectedResource: inMock.NewProvider(suite.user, suite.user),
Selector: sel.Selector, Selector: sel.Selector,
} }
@ -709,7 +721,7 @@ func (suite *BackupIntgSuite) TestMailSerializationRegression() {
ctx, ctx,
bpc, bpc,
handlers, handlers,
suite.m365.TenantID, suite.tenantID,
sel.Scopes()[0], sel.Scopes()[0],
metadata.DeltaPaths{}, metadata.DeltaPaths{},
newStatusUpdater(t, &wg), newStatusUpdater(t, &wg),
@ -761,8 +773,8 @@ func (suite *BackupIntgSuite) TestMailSerializationRegression() {
// a regression test to ensure that downloaded items can be uploaded. // a regression test to ensure that downloaded items can be uploaded.
func (suite *BackupIntgSuite) TestContactSerializationRegression() { func (suite *BackupIntgSuite) TestContactSerializationRegression() {
var ( var (
users = []string{suite.m365.User.ID} users = []string{suite.user}
handlers = BackupHandlers(suite.m365.AC) handlers = BackupHandlers(suite.ac)
) )
tests := []struct { tests := []struct {
@ -789,14 +801,14 @@ func (suite *BackupIntgSuite) TestContactSerializationRegression() {
bpc := inject.BackupProducerConfig{ bpc := inject.BackupProducerConfig{
LastBackupVersion: version.NoBackup, LastBackupVersion: version.NoBackup,
Options: control.DefaultOptions(), Options: control.DefaultOptions(),
ProtectedResource: suite.m365.User.Provider, ProtectedResource: inMock.NewProvider(suite.user, suite.user),
} }
edcs, err := CreateCollections( edcs, err := CreateCollections(
ctx, ctx,
bpc, bpc,
handlers, handlers,
suite.m365.TenantID, suite.tenantID,
test.scope, test.scope,
metadata.DeltaPaths{}, metadata.DeltaPaths{},
newStatusUpdater(t, &wg), newStatusUpdater(t, &wg),
@ -863,8 +875,8 @@ func (suite *BackupIntgSuite) TestContactSerializationRegression() {
// to be able to successfully query, download and restore event objects // to be able to successfully query, download and restore event objects
func (suite *BackupIntgSuite) TestEventsSerializationRegression() { func (suite *BackupIntgSuite) TestEventsSerializationRegression() {
var ( var (
users = []string{suite.m365.User.ID} users = []string{suite.user}
handlers = BackupHandlers(suite.m365.AC) handlers = BackupHandlers(suite.ac)
) )
tests := []struct { tests := []struct {
@ -899,14 +911,14 @@ func (suite *BackupIntgSuite) TestEventsSerializationRegression() {
bpc := inject.BackupProducerConfig{ bpc := inject.BackupProducerConfig{
LastBackupVersion: version.NoBackup, LastBackupVersion: version.NoBackup,
Options: control.DefaultOptions(), Options: control.DefaultOptions(),
ProtectedResource: suite.m365.User.Provider, ProtectedResource: inMock.NewProvider(suite.user, suite.user),
} }
collections, err := CreateCollections( collections, err := CreateCollections(
ctx, ctx,
bpc, bpc,
handlers, handlers,
suite.m365.TenantID, suite.tenantID,
test.scope, test.scope,
metadata.DeltaPaths{}, metadata.DeltaPaths{},
newStatusUpdater(t, &wg), newStatusUpdater(t, &wg),

View File

@ -19,7 +19,6 @@ import (
"github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/m365/support"
"github.com/alcionai/corso/src/internal/observe" "github.com/alcionai/corso/src/internal/observe"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/count" "github.com/alcionai/corso/src/pkg/count"
"github.com/alcionai/corso/src/pkg/errs/core" "github.com/alcionai/corso/src/pkg/errs/core"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
@ -69,21 +68,21 @@ func getItemAndInfo(
ctx context.Context, ctx context.Context,
getter itemGetterSerializer, getter itemGetterSerializer,
userID string, userID string,
itemID string, id string,
useImmutableIDs bool, useImmutableIDs bool,
parentPath string, parentPath string,
) ([]byte, *details.ExchangeInfo, error) { ) ([]byte, *details.ExchangeInfo, error) {
item, info, err := getter.GetItem( item, info, err := getter.GetItem(
ctx, ctx,
userID, userID,
itemID, id,
fault.New(true)) // temporary way to force a failFast error fault.New(true)) // temporary way to force a failFast error
if err != nil { if err != nil {
return nil, nil, clues.WrapWC(ctx, err, "fetching item"). return nil, nil, clues.WrapWC(ctx, err, "fetching item").
Label(fault.LabelForceNoBackupCreation) Label(fault.LabelForceNoBackupCreation)
} }
itemData, err := getter.Serialize(ctx, item, userID, itemID) itemData, err := getter.Serialize(ctx, item, userID, id)
if err != nil { if err != nil {
return nil, nil, clues.WrapWC(ctx, err, "serializing item") return nil, nil, clues.WrapWC(ctx, err, "serializing item")
} }
@ -109,7 +108,6 @@ func NewCollection(
bc data.BaseCollection, bc data.BaseCollection,
user string, user string,
items itemGetterSerializer, items itemGetterSerializer,
canSkipFailChecker canSkipItemFailurer,
origAdded map[string]time.Time, origAdded map[string]time.Time,
origRemoved []string, origRemoved []string,
validModTimes bool, validModTimes bool,
@ -142,7 +140,6 @@ func NewCollection(
added: added, added: added,
removed: removed, removed: removed,
getter: items, getter: items,
skipChecker: canSkipFailChecker,
statusUpdater: statusUpdater, statusUpdater: statusUpdater,
} }
} }
@ -153,7 +150,6 @@ func NewCollection(
added: added, added: added,
removed: removed, removed: removed,
getter: items, getter: items,
skipChecker: canSkipFailChecker,
statusUpdater: statusUpdater, statusUpdater: statusUpdater,
counter: counter, counter: counter,
} }
@ -172,7 +168,6 @@ type prefetchCollection struct {
removed map[string]struct{} removed map[string]struct{}
getter itemGetterSerializer getter itemGetterSerializer
skipChecker canSkipItemFailurer
statusUpdater support.StatusUpdater statusUpdater support.StatusUpdater
} }
@ -199,12 +194,11 @@ func (col *prefetchCollection) streamItems(
wg sync.WaitGroup wg sync.WaitGroup
progressMessage chan<- struct{} progressMessage chan<- struct{}
user = col.user user = col.user
dataCategory = col.Category().String()
) )
ctx = clues.Add( ctx = clues.Add(
ctx, ctx,
"category", dataCategory) "category", col.Category().String())
defer func() { defer func() {
close(stream) close(stream)
@ -233,7 +227,7 @@ func (col *prefetchCollection) streamItems(
defer close(semaphoreCh) defer close(semaphoreCh)
// delete all removed items // delete all removed items
for itemID := range col.removed { for id := range col.removed {
semaphoreCh <- struct{}{} semaphoreCh <- struct{}{}
wg.Add(1) wg.Add(1)
@ -253,7 +247,7 @@ func (col *prefetchCollection) streamItems(
if progressMessage != nil { if progressMessage != nil {
progressMessage <- struct{}{} progressMessage <- struct{}{}
} }
}(itemID) }(id)
} }
var ( var (
@ -262,7 +256,7 @@ func (col *prefetchCollection) streamItems(
) )
// add any new items // add any new items
for itemID := range col.added { for id := range col.added {
if el.Failure() != nil { if el.Failure() != nil {
break break
} }
@ -283,23 +277,8 @@ func (col *prefetchCollection) streamItems(
col.Opts().ToggleFeatures.ExchangeImmutableIDs, col.Opts().ToggleFeatures.ExchangeImmutableIDs,
parentPath) parentPath)
if err != nil { if err != nil {
// pulled outside the switch due to multiple return values.
cause, canSkip := col.skipChecker.CanSkipItemFailure(
err,
user,
col.Opts())
// Handle known error cases // Handle known error cases
switch { switch {
case canSkip:
// this is a special case handler that allows the item to be skipped
// instead of producing an error.
errs.AddSkip(ctx, fault.FileSkip(
cause,
dataCategory,
id,
id,
nil))
case errors.Is(err, core.ErrNotFound): case errors.Is(err, core.ErrNotFound):
// Don't report errors for deleted items as there's no way for us to // Don't report errors for deleted items as there's no way for us to
// back up data that is gone. Record it as a "success", since there's // back up data that is gone. Record it as a "success", since there's
@ -321,19 +300,6 @@ func (col *prefetchCollection) streamItems(
id, id,
map[string]any{"parentPath": parentPath})) map[string]any{"parentPath": parentPath}))
atomic.AddInt64(&success, 1) atomic.AddInt64(&success, 1)
case graph.IsErrCorruptData(err):
// These items cannot be downloaded, graph error indicates that the item
// data is corrupted. Add to skipped list.
logger.
CtxErr(ctx, err).
With("skipped_reason", fault.SkipCorruptData).
Info("inaccessible email")
errs.AddSkip(ctx, fault.EmailSkip(
fault.SkipCorruptData,
user,
id,
map[string]any{"parentPath": parentPath}))
atomic.AddInt64(&success, 1)
default: default:
col.Counter.Inc(count.StreamItemsErred) col.Counter.Inc(count.StreamItemsErred)
el.AddRecoverable(ctx, clues.Wrap(err, "fetching item").Label(fault.LabelForceNoBackupCreation)) el.AddRecoverable(ctx, clues.Wrap(err, "fetching item").Label(fault.LabelForceNoBackupCreation))
@ -370,7 +336,7 @@ func (col *prefetchCollection) streamItems(
if progressMessage != nil { if progressMessage != nil {
progressMessage <- struct{}{} progressMessage <- struct{}{}
} }
}(itemID) }(id)
} }
wg.Wait() wg.Wait()
@ -399,7 +365,6 @@ type lazyFetchCollection struct {
removed map[string]struct{} removed map[string]struct{}
getter itemGetterSerializer getter itemGetterSerializer
skipChecker canSkipItemFailurer
statusUpdater support.StatusUpdater statusUpdater support.StatusUpdater
@ -426,8 +391,8 @@ func (col *lazyFetchCollection) streamItems(
var ( var (
success int64 success int64
progressMessage chan<- struct{} progressMessage chan<- struct{}
user = col.user user = col.user
el = errs.Local()
) )
defer func() { defer func() {
@ -439,7 +404,7 @@ func (col *lazyFetchCollection) streamItems(
int(success), int(success),
0, 0,
col.FullPath().Folder(false), col.FullPath().Folder(false),
el.Failure()) errs.Failure())
}() }()
if len(col.added)+len(col.removed) > 0 { if len(col.added)+len(col.removed) > 0 {
@ -465,7 +430,7 @@ func (col *lazyFetchCollection) streamItems(
// add any new items // add any new items
for id, modTime := range col.added { for id, modTime := range col.added {
if el.Failure() != nil { if errs.Failure() != nil {
break break
} }
@ -481,18 +446,15 @@ func (col *lazyFetchCollection) streamItems(
&lazyItemGetter{ &lazyItemGetter{
userID: user, userID: user,
itemID: id, itemID: id,
category: col.Category(),
getter: col.getter, getter: col.getter,
modTime: modTime, modTime: modTime,
immutableIDs: col.Opts().ToggleFeatures.ExchangeImmutableIDs, immutableIDs: col.Opts().ToggleFeatures.ExchangeImmutableIDs,
parentPath: parentPath, parentPath: parentPath,
skipChecker: col.skipChecker,
opts: col.Opts(),
}, },
id, id,
modTime, modTime,
col.counter, col.counter,
el) errs)
atomic.AddInt64(&success, 1) atomic.AddInt64(&success, 1)
@ -506,12 +468,9 @@ type lazyItemGetter struct {
getter itemGetterSerializer getter itemGetterSerializer
userID string userID string
itemID string itemID string
category path.CategoryType
parentPath string parentPath string
modTime time.Time modTime time.Time
immutableIDs bool immutableIDs bool
skipChecker canSkipItemFailurer
opts control.Options
} }
func (lig *lazyItemGetter) GetData( func (lig *lazyItemGetter) GetData(
@ -526,25 +485,6 @@ func (lig *lazyItemGetter) GetData(
lig.immutableIDs, lig.immutableIDs,
lig.parentPath) lig.parentPath)
if err != nil { if err != nil {
if lig.skipChecker != nil {
cause, canSkip := lig.skipChecker.CanSkipItemFailure(
err,
lig.userID,
lig.opts)
if canSkip {
errs.AddSkip(ctx, fault.FileSkip(
cause,
lig.category.String(),
lig.itemID,
lig.itemID,
nil))
return nil, nil, false, clues.
NewWC(ctx, "error marked as skippable by handler").
Label(graph.LabelsSkippable)
}
}
// If an item was deleted then return an empty file so we don't fail // If an item was deleted then return an empty file so we don't fail
// the backup and return a sentinel error when asked for ItemInfo so // the backup and return a sentinel error when asked for ItemInfo so
// we don't display the item in the backup. // we don't display the item in the backup.
@ -559,7 +499,7 @@ func (lig *lazyItemGetter) GetData(
err = clues.Stack(err) err = clues.Stack(err)
errs.AddRecoverable(ctx, err) errs.AddRecoverable(ctx, err)
return nil, nil, false, clues.Stack(err) return nil, nil, false, err
} }
// Update the mod time to what we already told kopia about. This is required // Update the mod time to what we already told kopia about. This is required

View File

@ -28,7 +28,6 @@ import (
"github.com/alcionai/corso/src/pkg/errs/core" "github.com/alcionai/corso/src/pkg/errs/core"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/services/m365/api"
"github.com/alcionai/corso/src/pkg/services/m365/api/graph" "github.com/alcionai/corso/src/pkg/services/m365/api/graph"
graphTD "github.com/alcionai/corso/src/pkg/services/m365/api/graph/testdata" graphTD "github.com/alcionai/corso/src/pkg/services/m365/api/graph/testdata"
) )
@ -154,7 +153,6 @@ func (suite *CollectionUnitSuite) TestNewCollection_state() {
count.New()), count.New()),
"u", "u",
mock.DefaultItemGetSerialize(), mock.DefaultItemGetSerialize(),
mock.NeverCanSkipFailChecker(),
nil, nil,
nil, nil,
colType.validModTimes, colType.validModTimes,
@ -300,7 +298,6 @@ func (suite *CollectionUnitSuite) TestPrefetchCollection_Items() {
count.New()), count.New()),
"", "",
&mock.ItemGetSerialize{}, &mock.ItemGetSerialize{},
mock.NeverCanSkipFailChecker(),
test.added, test.added,
maps.Keys(test.removed), maps.Keys(test.removed),
false, false,
@ -336,232 +333,6 @@ func (suite *CollectionUnitSuite) TestPrefetchCollection_Items() {
} }
} }
func (suite *CollectionUnitSuite) TestPrefetchCollection_Items_skipFailure() {
var (
start = time.Now().Add(-time.Second)
statusUpdater = func(*support.ControllerOperationStatus) {}
)
table := []struct {
name string
category path.CategoryType
handler backupHandler
added map[string]time.Time
removed map[string]struct{}
expectItemCount int
expectSkippedCount int
expectErr assert.ErrorAssertionFunc
}{
{
name: "no items",
category: path.EventsCategory,
handler: newEventBackupHandler(api.Client{}),
expectErr: assert.NoError,
},
{
name: "events only added items",
category: path.EventsCategory,
handler: newEventBackupHandler(api.Client{}),
added: map[string]time.Time{
"fisher": {},
"flannigan": {},
"fitzbog": {},
},
expectItemCount: 0,
expectSkippedCount: 3,
expectErr: assert.NoError,
},
{
name: "events only removed items",
category: path.EventsCategory,
handler: newEventBackupHandler(api.Client{}),
removed: map[string]struct{}{
"princess": {},
"poppy": {},
"petunia": {},
},
expectItemCount: 3,
expectSkippedCount: 0,
expectErr: assert.NoError,
},
{
name: "events added and removed items",
category: path.EventsCategory,
handler: newEventBackupHandler(api.Client{}),
added: map[string]time.Time{
"general": {},
},
removed: map[string]struct{}{
"general": {},
"goose": {},
"grumbles": {},
},
expectItemCount: 3,
// not 1, because general is removed from the added
// map due to being in the removed map
expectSkippedCount: 0,
expectErr: assert.NoError,
},
{
name: "contacts only added items",
category: path.ContactsCategory,
handler: newContactBackupHandler(api.Client{}),
added: map[string]time.Time{
"fisher": {},
"flannigan": {},
"fitzbog": {},
},
expectItemCount: 0,
expectSkippedCount: 0,
expectErr: assert.Error,
},
{
name: "contacts only removed items",
category: path.ContactsCategory,
handler: newContactBackupHandler(api.Client{}),
removed: map[string]struct{}{
"princess": {},
"poppy": {},
"petunia": {},
},
expectItemCount: 3,
expectSkippedCount: 0,
expectErr: assert.NoError,
},
{
name: "contacts added and removed items",
category: path.ContactsCategory,
handler: newContactBackupHandler(api.Client{}),
added: map[string]time.Time{
"general": {},
},
removed: map[string]struct{}{
"general": {},
"goose": {},
"grumbles": {},
},
expectItemCount: 3,
// not 1, because general is removed from the added
// map due to being in the removed map
expectSkippedCount: 0,
expectErr: assert.NoError,
},
{
name: "mail only added items",
category: path.EmailCategory,
handler: newMailBackupHandler(api.Client{}),
added: map[string]time.Time{
"fisher": {},
"flannigan": {},
"fitzbog": {},
},
expectItemCount: 0,
expectSkippedCount: 0,
expectErr: assert.Error,
},
{
name: "mail only removed items",
category: path.EmailCategory,
handler: newMailBackupHandler(api.Client{}),
removed: map[string]struct{}{
"princess": {},
"poppy": {},
"petunia": {},
},
expectItemCount: 3,
expectSkippedCount: 0,
expectErr: assert.NoError,
},
{
name: "mail added and removed items",
category: path.EmailCategory,
handler: newMailBackupHandler(api.Client{}),
added: map[string]time.Time{
"general": {},
},
removed: map[string]struct{}{
"general": {},
"goose": {},
"grumbles": {},
},
expectItemCount: 3,
// not 1, because general is removed from the added
// map due to being in the removed map
expectSkippedCount: 0,
expectErr: assert.NoError,
},
}
for _, test := range table {
suite.Run(test.name, func() {
var (
t = suite.T()
errs = fault.New(true)
itemCount int
)
ctx, flush := tester.NewContext(t)
defer flush()
fullPath, err := path.Build("t", "pr", path.ExchangeService, test.category, false, "fnords", "smarf")
require.NoError(t, err, clues.ToCore(err))
locPath, err := path.Build("t", "pr", path.ExchangeService, test.category, false, "fnords", "smarf")
require.NoError(t, err, clues.ToCore(err))
opts := control.DefaultOptions()
opts.SkipEventsOnInstance503ForResources = map[string]struct{}{}
opts.SkipEventsOnInstance503ForResources["pr"] = struct{}{}
col := NewCollection(
data.NewBaseCollection(
fullPath,
nil,
locPath.ToBuilder(),
opts,
false,
count.New()),
"pr",
&mock.ItemGetSerialize{
SerializeErr: graph.ErrServiceUnavailableEmptyResp,
},
test.handler,
test.added,
maps.Keys(test.removed),
false,
statusUpdater,
count.New())
for item := range col.Items(ctx, errs) {
itemCount++
_, rok := test.removed[item.ID()]
if rok {
dimt, ok := item.(data.ItemModTime)
require.True(t, ok, "item implements data.ItemModTime")
assert.True(t, dimt.ModTime().After(start), "deleted items should set mod time to now()")
assert.True(t, item.Deleted(), "removals should be marked as deleted")
}
_, aok := test.added[item.ID()]
if !rok && aok {
assert.False(t, item.Deleted(), "additions should not be marked as deleted")
}
assert.True(t, aok || rok, "item must be either added or removed: %q", item.ID())
}
test.expectErr(t, errs.Failure())
assert.Equal(
t,
test.expectItemCount,
itemCount,
"should see all expected items")
assert.Len(t, errs.Skipped(), test.expectSkippedCount)
})
}
}
// This test verifies skipped error cases are handled correctly by collection enumeration // This test verifies skipped error cases are handled correctly by collection enumeration
func (suite *CollectionUnitSuite) TestCollection_SkippedErrors() { func (suite *CollectionUnitSuite) TestCollection_SkippedErrors() {
var ( var (
@ -593,17 +364,6 @@ func (suite *CollectionUnitSuite) TestCollection_SkippedErrors() {
}, },
expectedSkipError: fault.EmailSkip(fault.SkipInvalidRecipients, "", "fisher", nil), expectedSkipError: fault.EmailSkip(fault.SkipInvalidRecipients, "", "fisher", nil),
}, },
{
name: "ErrorCorruptData",
added: map[string]time.Time{
"fisher": {},
},
expectItemCount: 0,
itemGetter: &mock.ItemGetSerialize{
GetErr: graphTD.ODataErr(string(graph.ErrorCorruptData)),
},
expectedSkipError: fault.EmailSkip(fault.SkipCorruptData, "", "fisher", nil),
},
} }
for _, test := range table { for _, test := range table {
@ -627,7 +387,6 @@ func (suite *CollectionUnitSuite) TestCollection_SkippedErrors() {
count.New()), count.New()),
"", "",
test.itemGetter, test.itemGetter,
mock.NeverCanSkipFailChecker(),
test.added, test.added,
nil, nil,
false, false,
@ -708,7 +467,6 @@ func (suite *CollectionUnitSuite) TestLazyFetchCollection_Items_LazyFetch() {
expectItemCount: 3, expectItemCount: 3,
expectReads: []string{ expectReads: []string{
"fisher", "fisher",
"flannigan",
"fitzbog", "fitzbog",
}, },
}, },
@ -761,7 +519,6 @@ func (suite *CollectionUnitSuite) TestLazyFetchCollection_Items_LazyFetch() {
count.New()), count.New()),
"", "",
mlg, mlg,
mock.NeverCanSkipFailChecker(),
test.added, test.added,
maps.Keys(test.removed), maps.Keys(test.removed),
true, true,
@ -773,10 +530,10 @@ func (suite *CollectionUnitSuite) TestLazyFetchCollection_Items_LazyFetch() {
_, rok := test.removed[item.ID()] _, rok := test.removed[item.ID()]
if rok { if rok {
assert.True(t, item.Deleted(), "removals should be marked as deleted")
dimt, ok := item.(data.ItemModTime) dimt, ok := item.(data.ItemModTime)
require.True(t, ok, "item implements data.ItemModTime") require.True(t, ok, "item implements data.ItemModTime")
assert.True(t, dimt.ModTime().After(start), "deleted items should set mod time to now()") assert.True(t, dimt.ModTime().After(start), "deleted items should set mod time to now()")
assert.True(t, item.Deleted(), "removals should be marked as deleted")
} }
modTime, aok := test.added[item.ID()] modTime, aok := test.added[item.ID()]
@ -785,6 +542,7 @@ func (suite *CollectionUnitSuite) TestLazyFetchCollection_Items_LazyFetch() {
// initializer. // initializer.
assert.Implements(t, (*data.ItemModTime)(nil), item) assert.Implements(t, (*data.ItemModTime)(nil), item)
assert.Equal(t, modTime, item.(data.ItemModTime).ModTime(), "item mod time") assert.Equal(t, modTime, item.(data.ItemModTime).ModTime(), "item mod time")
assert.False(t, item.Deleted(), "additions should not be marked as deleted") assert.False(t, item.Deleted(), "additions should not be marked as deleted")
// Check if the test want's us to read the item's data so the lazy // Check if the test want's us to read the item's data so the lazy
@ -804,8 +562,6 @@ func (suite *CollectionUnitSuite) TestLazyFetchCollection_Items_LazyFetch() {
// collection initializer. // collection initializer.
assert.NoError(t, err, clues.ToCore(err)) assert.NoError(t, err, clues.ToCore(err))
assert.Equal(t, modTime, info.Modified(), "ItemInfo mod time") assert.Equal(t, modTime, info.Modified(), "ItemInfo mod time")
} else {
assert.Fail(t, "unexpected read on item %s", item.ID())
} }
} }
@ -822,294 +578,6 @@ func (suite *CollectionUnitSuite) TestLazyFetchCollection_Items_LazyFetch() {
} }
} }
func (suite *CollectionUnitSuite) TestLazyFetchCollection_Items_skipFailure() {
var (
start = time.Now().Add(-time.Second)
statusUpdater = func(*support.ControllerOperationStatus) {}
expectSkip = func(t *testing.T, err error) {
assert.Error(t, err, clues.ToCore(err))
assert.ErrorContains(t, err, "skip")
assert.True(t, clues.HasLabel(err, graph.LabelsSkippable), clues.ToCore(err))
}
expectNotSkipped = func(t *testing.T, err error) {
assert.Error(t, err, clues.ToCore(err))
assert.NotContains(t, err.Error(), "skip")
}
)
table := []struct {
name string
added map[string]time.Time
removed map[string]struct{}
category path.CategoryType
handler backupHandler
expectItemCount int
expectSkippedCount int
expectReads []string
expectErr func(t *testing.T, err error)
expectFailure assert.ErrorAssertionFunc
}{
{
name: "no items",
category: path.EventsCategory,
handler: newEventBackupHandler(api.Client{}),
expectFailure: assert.NoError,
},
{
name: "events only added items",
category: path.EventsCategory,
handler: newEventBackupHandler(api.Client{}),
added: map[string]time.Time{
"fisher": start.Add(time.Minute),
"flannigan": start.Add(2 * time.Minute),
"fitzbog": start.Add(3 * time.Minute),
},
expectItemCount: 3,
expectSkippedCount: 3,
expectReads: []string{
"fisher",
"flannigan",
"fitzbog",
},
expectErr: expectSkip,
expectFailure: assert.NoError,
},
{
name: "events only removed items",
category: path.EventsCategory,
handler: newEventBackupHandler(api.Client{}),
removed: map[string]struct{}{
"princess": {},
"poppy": {},
"petunia": {},
},
expectItemCount: 3,
expectSkippedCount: 0,
expectErr: expectSkip,
expectFailure: assert.NoError,
},
{
name: "events added and removed items",
category: path.EventsCategory,
handler: newEventBackupHandler(api.Client{}),
added: map[string]time.Time{
"general": {},
},
removed: map[string]struct{}{
"general": {},
"goose": {},
"grumbles": {},
},
expectItemCount: 3,
// not 1, because general is removed from the added
// map due to being in the removed map
expectSkippedCount: 0,
expectErr: expectSkip,
expectFailure: assert.NoError,
},
{
name: "contacts only added items",
category: path.ContactsCategory,
handler: newContactBackupHandler(api.Client{}),
added: map[string]time.Time{
"fisher": start.Add(time.Minute),
"flannigan": start.Add(2 * time.Minute),
"fitzbog": start.Add(3 * time.Minute),
},
expectItemCount: 3,
expectSkippedCount: 0,
expectReads: []string{
"fisher",
"flannigan",
"fitzbog",
},
expectErr: expectNotSkipped,
expectFailure: assert.Error,
},
{
name: "contacts only removed items",
category: path.ContactsCategory,
handler: newContactBackupHandler(api.Client{}),
removed: map[string]struct{}{
"princess": {},
"poppy": {},
"petunia": {},
},
expectItemCount: 3,
expectSkippedCount: 0,
expectErr: expectNotSkipped,
expectFailure: assert.NoError,
},
{
name: "contacts added and removed items",
category: path.ContactsCategory,
handler: newContactBackupHandler(api.Client{}),
added: map[string]time.Time{
"general": {},
},
removed: map[string]struct{}{
"general": {},
"goose": {},
"grumbles": {},
},
expectItemCount: 3,
// not 1, because general is removed from the added
// map due to being in the removed map
expectSkippedCount: 0,
expectErr: expectNotSkipped,
expectFailure: assert.NoError,
},
{
name: "mail only added items",
category: path.EmailCategory,
handler: newMailBackupHandler(api.Client{}),
added: map[string]time.Time{
"fisher": start.Add(time.Minute),
"flannigan": start.Add(2 * time.Minute),
"fitzbog": start.Add(3 * time.Minute),
},
expectItemCount: 3,
expectSkippedCount: 0,
expectReads: []string{
"fisher",
"flannigan",
"fitzbog",
},
expectErr: expectNotSkipped,
expectFailure: assert.Error,
},
{
name: "mail only removed items",
category: path.EmailCategory,
handler: newMailBackupHandler(api.Client{}),
removed: map[string]struct{}{
"princess": {},
"poppy": {},
"petunia": {},
},
expectItemCount: 3,
expectSkippedCount: 0,
expectErr: expectNotSkipped,
expectFailure: assert.NoError,
},
{
name: "mail added and removed items",
category: path.EmailCategory,
handler: newMailBackupHandler(api.Client{}),
added: map[string]time.Time{
"general": {},
},
removed: map[string]struct{}{
"general": {},
"goose": {},
"grumbles": {},
},
expectItemCount: 3,
// not 1, because general is removed from the added
// map due to being in the removed map
expectSkippedCount: 0,
expectErr: expectNotSkipped,
expectFailure: assert.NoError,
},
}
for _, test := range table {
suite.Run(test.name, func() {
var (
t = suite.T()
errs = fault.New(false)
itemCount int
)
ctx, flush := tester.NewContext(t)
defer flush()
fullPath, err := path.Build("t", "pr", path.ExchangeService, test.category, false, "fnords", "smarf")
require.NoError(t, err, clues.ToCore(err))
locPath, err := path.Build("t", "pr", path.ExchangeService, test.category, false, "fnords", "smarf")
require.NoError(t, err, clues.ToCore(err))
mlg := &mockLazyItemGetterSerializer{
ItemGetSerialize: &mock.ItemGetSerialize{
SerializeErr: graph.ErrServiceUnavailableEmptyResp,
},
}
defer mlg.check(t, test.expectReads)
opts := control.DefaultOptions()
opts.SkipEventsOnInstance503ForResources = map[string]struct{}{}
opts.SkipEventsOnInstance503ForResources["pr"] = struct{}{}
col := NewCollection(
data.NewBaseCollection(
fullPath,
nil,
locPath.ToBuilder(),
opts,
false,
count.New()),
"pr",
mlg,
test.handler,
test.added,
maps.Keys(test.removed),
true,
statusUpdater,
count.New())
for item := range col.Items(ctx, errs) {
itemCount++
_, rok := test.removed[item.ID()]
if rok {
dimt, ok := item.(data.ItemModTime)
require.True(t, ok, "item implements data.ItemModTime")
assert.True(t, dimt.ModTime().After(start), "deleted items should set mod time to now()")
assert.True(t, item.Deleted(), "removals should be marked as deleted")
}
modTime, aok := test.added[item.ID()]
if !rok && aok {
// Item's mod time should be what's passed into the collection
// initializer.
assert.Implements(t, (*data.ItemModTime)(nil), item)
assert.Equal(t, modTime, item.(data.ItemModTime).ModTime(), "item mod time")
assert.False(t, item.Deleted(), "additions should not be marked as deleted")
// Check if the test want's us to read the item's data so the lazy
// data fetch is executed.
if slices.Contains(test.expectReads, item.ID()) {
r := item.ToReader()
_, err := io.ReadAll(r)
test.expectErr(t, err)
r.Close()
} else {
assert.Fail(t, "unexpected read on item %s", item.ID())
}
}
assert.True(t, aok || rok, "item must be either added or removed: %q", item.ID())
}
failure := errs.Failure()
if failure == nil && len(errs.Recovered()) > 0 {
failure = errs.Recovered()[0]
}
test.expectFailure(t, failure, clues.ToCore(failure))
assert.Equal(
t,
test.expectItemCount,
itemCount,
"should see all expected items")
assert.Len(t, errs.Skipped(), test.expectSkippedCount)
})
}
}
func (suite *CollectionUnitSuite) TestLazyItem_NoRead_GetInfo_Errors() { func (suite *CollectionUnitSuite) TestLazyItem_NoRead_GetInfo_Errors() {
t := suite.T() t := suite.T()

View File

@ -1,8 +1,6 @@
package exchange package exchange
import ( import (
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/services/m365/api" "github.com/alcionai/corso/src/pkg/services/m365/api"
"github.com/alcionai/corso/src/pkg/services/m365/api/graph" "github.com/alcionai/corso/src/pkg/services/m365/api/graph"
) )
@ -54,11 +52,3 @@ func (h contactBackupHandler) NewContainerCache(
getter: h.ac, getter: h.ac,
} }
} }
func (h contactBackupHandler) CanSkipItemFailure(
err error,
resourceID string,
opts control.Options,
) (fault.SkipCause, bool) {
return "", false
}

View File

@ -1,83 +0,0 @@
package exchange
import (
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
type ContactsBackupHandlerUnitSuite struct {
tester.Suite
}
func TestContactsBackupHandlerUnitSuite(t *testing.T) {
suite.Run(t, &ContactsBackupHandlerUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *ContactsBackupHandlerUnitSuite) TestHandler_CanSkipItemFailure() {
resourceID := uuid.NewString()
table := []struct {
name string
err error
opts control.Options
expect assert.BoolAssertionFunc
expectCause fault.SkipCause
}{
{
name: "no config",
err: assert.AnError,
opts: control.Options{},
expect: assert.False,
},
{
name: "false when map is empty",
err: assert.AnError,
opts: control.Options{
SkipEventsOnInstance503ForResources: map[string]struct{}{},
},
expect: assert.False,
},
{
name: "false on nil error",
err: nil,
opts: control.Options{
SkipEventsOnInstance503ForResources: map[string]struct{}{
resourceID: {},
},
},
expect: assert.False,
},
{
name: "false even if resource matches",
err: assert.AnError,
opts: control.Options{
SkipEventsOnInstance503ForResources: map[string]struct{}{
resourceID: {},
},
},
expect: assert.False,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
h := newContactBackupHandler(api.Client{})
cause, result := h.CanSkipItemFailure(
test.err,
resourceID,
test.opts)
test.expect(t, result)
assert.Equal(t, test.expectCause, cause)
})
}
}

View File

@ -126,7 +126,7 @@ func (cfc *contactContainerCache) Populate(
if err != nil { if err != nil {
errs.AddRecoverable( errs.AddRecoverable(
ctx, ctx,
clues.StackWC(ctx, err).Label(fault.LabelForceNoBackupCreation)) graph.Stack(ctx, err).Label(fault.LabelForceNoBackupCreation))
} }
} }

View File

@ -120,7 +120,7 @@ func restoreContact(
) (*details.ExchangeInfo, error) { ) (*details.ExchangeInfo, error) {
contact, err := api.BytesToContactable(body) contact, err := api.BytesToContactable(body)
if err != nil { if err != nil {
return nil, clues.WrapWC(ctx, err, "creating contact from bytes") return nil, graph.Wrap(ctx, err, "creating contact from bytes")
} }
ctx = clues.Add(ctx, "item_id", ptr.Val(contact.GetId())) ctx = clues.Add(ctx, "item_id", ptr.Val(contact.GetId()))
@ -148,7 +148,7 @@ func restoreContact(
item, err := cr.PostItem(ctx, userID, destinationID, contact) item, err := cr.PostItem(ctx, userID, destinationID, contact)
if err != nil { if err != nil {
return nil, clues.Wrap(err, "restoring contact") return nil, graph.Wrap(ctx, err, "restoring contact")
} }
// contacts have no PUT request, and PATCH could retain data that's not // contacts have no PUT request, and PATCH could retain data that's not
@ -159,7 +159,7 @@ func restoreContact(
if shouldDeleteOriginal { if shouldDeleteOriginal {
err := cr.DeleteItem(ctx, userID, collisionID) err := cr.DeleteItem(ctx, userID, collisionID)
if err != nil && !errors.Is(err, core.ErrNotFound) { if err != nil && !errors.Is(err, core.ErrNotFound) {
return nil, clues.Wrap(err, "deleting colliding contact") return nil, graph.Wrap(ctx, err, "deleting colliding contact")
} }
} }

View File

@ -12,7 +12,6 @@ import (
"github.com/alcionai/corso/src/internal/m365/service/exchange/mock" "github.com/alcionai/corso/src/internal/m365/service/exchange/mock"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/control/testdata" "github.com/alcionai/corso/src/pkg/control/testdata"
@ -55,7 +54,7 @@ func (m *contactRestoreMock) DeleteItem(
type ContactsRestoreIntgSuite struct { type ContactsRestoreIntgSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup its intgTesterSetup
} }
func TestContactsRestoreIntgSuite(t *testing.T) { func TestContactsRestoreIntgSuite(t *testing.T) {
@ -67,17 +66,17 @@ func TestContactsRestoreIntgSuite(t *testing.T) {
} }
func (suite *ContactsRestoreIntgSuite) SetupSuite() { func (suite *ContactsRestoreIntgSuite) SetupSuite() {
suite.m365 = its.GetM365(suite.T()) suite.its = newIntegrationTesterSetup(suite.T())
} }
// Testing to ensure that cache system works for in multiple different environments // Testing to ensure that cache system works for in multiple different environments
func (suite *ContactsRestoreIntgSuite) TestCreateContainerDestination() { func (suite *ContactsRestoreIntgSuite) TestCreateContainerDestination() {
runCreateDestinationTest( runCreateDestinationTest(
suite.T(), suite.T(),
newContactRestoreHandler(suite.m365.AC), newContactRestoreHandler(suite.its.ac),
path.ContactsCategory, path.ContactsCategory,
suite.m365.TenantID, suite.its.creds.AzureTenantID,
suite.m365.User.ID, suite.its.userID,
testdata.DefaultRestoreConfig("").Location, testdata.DefaultRestoreConfig("").Location,
[]string{"Hufflepuff"}, []string{"Hufflepuff"},
[]string{"Ravenclaw"}) []string{"Ravenclaw"})
@ -208,16 +207,17 @@ func (suite *ContactsRestoreIntgSuite) TestRestoreContact() {
for _, test := range table { for _, test := range table {
suite.Run(test.name, func() { suite.Run(test.name, func() {
t := suite.T() t := suite.T()
ctr := count.New()
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
ctr := count.New()
_, err := restoreContact( _, err := restoreContact(
ctx, ctx,
test.apiMock, test.apiMock,
body, body,
suite.m365.User.ID, suite.its.userID,
"destination", "destination",
test.collisionMap, test.collisionMap,
test.onCollision, test.onCollision,

View File

@ -3,13 +3,11 @@ package exchange
import ( import (
"context" "context"
"fmt" "fmt"
"hash/crc32"
stdpath "path" stdpath "path"
"testing" "testing"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -18,8 +16,10 @@ import (
"github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/count"
"github.com/alcionai/corso/src/pkg/errs/core" "github.com/alcionai/corso/src/pkg/errs/core"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
@ -1019,241 +1019,49 @@ func (suite *ConfiguredFolderCacheUnitSuite) TestAddToCache() {
assert.Equal(t, m.expectedLocation, l.String(), "location path") assert.Equal(t, m.expectedLocation, l.String(), "location path")
} }
// --------------------------------------------------------------------------- type ContainerResolverSuite struct {
// EventContainerCache unit tests
// ---------------------------------------------------------------------------
var _ containerGetter = mockEventContainerGetter{}
type mockEventContainerGetter struct {
// containerGetter returns graph.CalendarDisplayable, unlike containersEnumerator
// which returns models.Calendarable.
idToCalendar map[string]graph.CalendarDisplayable
err error
}
func (m mockEventContainerGetter) GetContainerByID(
ctx context.Context,
userID string,
dirID string,
) (graph.Container, error) {
return m.idToCalendar[dirID], m.err
}
var _ containersEnumerator[models.Calendarable] = mockEventContainersEnumerator{}
type mockEventContainersEnumerator struct {
containers []models.Calendarable
err error
}
func (m mockEventContainersEnumerator) EnumerateContainers(
ctx context.Context,
userID string,
baseDirID string,
) ([]models.Calendarable, error) {
return m.containers, m.err
}
type EventsContainerUnitSuite struct {
tester.Suite tester.Suite
credentials account.M365Config
} }
func TestEventsContainerUnitSuite(t *testing.T) { func TestContainerResolverIntegrationSuite(t *testing.T) {
suite.Run(t, &EventsContainerUnitSuite{ suite.Run(t, &ContainerResolverSuite{
Suite: tester.NewUnitSuite(t),
})
}
func makeCalendar(
id, name, ownerEmail string,
isDefault bool,
) *models.Calendar {
c := models.NewCalendar()
c.SetId(ptr.To(id))
c.SetName(ptr.To(name))
c.SetIsDefaultCalendar(ptr.To(isDefault))
if len(ownerEmail) > 0 {
email := models.NewEmailAddress()
email.SetAddress(ptr.To(ownerEmail))
// Set crc as the name for keeping this func simple.
eName := fmt.Sprintf("%d", crc32.ChecksumIEEE([]byte(ownerEmail)))
email.SetName(ptr.To(eName))
c.SetOwner(email)
}
return c
}
// Test if we skip backup of shared calendars. These will be backed up for
// the resource owner that owns the calendar.
func (suite *EventsContainerUnitSuite) TestPopulate_SkipSharedCalendars() {
// map of calendars
calendars := map[string]models.Calendarable{
// Default calendars Dx
"D0": makeCalendar(api.DefaultCalendar, api.DefaultCalendar, "owner@bar.com", true),
// Atypical, but creating another default calendar for testing purposes.
"D1": makeCalendar("D1", "D1", "owner@bar.com", true),
// Shared calendars Sx
"S0": makeCalendar("S0", "S0", "sharer@bar.com", false),
// Owned calendars, not default Ox
"O0": makeCalendar("O0", "O0", "owner@bar.com", false),
// Calendars with missing owner informaton
"M0": makeCalendar("M0", "M0", "", false),
}
// Always return default calendar from the getter.
getContainersByID := func() map[string]graph.CalendarDisplayable {
return map[string]graph.CalendarDisplayable{
api.DefaultCalendar: *graph.CreateCalendarDisplayable(calendars["D0"], "parentID"),
}
}
table := []struct {
name string
enumerateContainers func() []models.Calendarable
expectErr assert.ErrorAssertionFunc
assertFunc func(t *testing.T, ecc *eventContainerCache)
}{
{
name: "one default calendar, one shared",
enumerateContainers: func() []models.Calendarable {
return []models.Calendarable{
calendars["D0"],
calendars["S0"],
}
},
expectErr: assert.NoError,
assertFunc: func(t *testing.T, ecc *eventContainerCache) {
assert.Len(t, ecc.cache, 1, "expected calendar count")
assert.NotNil(t, ecc.cache[api.DefaultCalendar], "missing default calendar")
},
},
{
name: "2 default calendars, 1 shared",
enumerateContainers: func() []models.Calendarable {
return []models.Calendarable{
calendars["D0"],
calendars["D1"],
calendars["S0"],
}
},
expectErr: assert.NoError,
assertFunc: func(t *testing.T, ecc *eventContainerCache) {
assert.Len(t, ecc.cache, 2, "expected calendar count")
assert.NotNil(t, ecc.cache[api.DefaultCalendar], "missing default calendar")
assert.NotNil(t, ecc.cache["D1"], "missing default calendar")
},
},
{
name: "1 default, 1 additional owned, 1 shared",
enumerateContainers: func() []models.Calendarable {
return []models.Calendarable{
calendars["D0"],
calendars["O0"],
calendars["S0"],
}
},
expectErr: assert.NoError,
assertFunc: func(t *testing.T, ecc *eventContainerCache) {
assert.Len(t, ecc.cache, 2, "expected calendar count")
assert.NotNil(t, ecc.cache[api.DefaultCalendar], "missing default calendar")
assert.NotNil(t, ecc.cache["O0"], "missing owned calendar")
},
},
{
name: "1 default, 1 with missing owner information",
enumerateContainers: func() []models.Calendarable {
return []models.Calendarable{
calendars["D0"],
calendars["M0"],
}
},
expectErr: assert.NoError,
assertFunc: func(t *testing.T, ecc *eventContainerCache) {
assert.Len(t, ecc.cache, 2, "expected calendar count")
assert.NotNil(t, ecc.cache[api.DefaultCalendar], "missing default calendar")
assert.NotNil(t, ecc.cache["M0"], "missing calendar with missing owner info")
},
},
{
// Unlikely to happen, but we should back up the calendar if the default owner
// cannot be determined, i.e. default calendar is missing.
name: "default owner info missing",
enumerateContainers: func() []models.Calendarable {
return []models.Calendarable{
calendars["S0"],
}
},
expectErr: assert.NoError,
assertFunc: func(t *testing.T, ecc *eventContainerCache) {
assert.Len(t, ecc.cache, 2, "expected calendar count")
assert.NotNil(t, ecc.cache[api.DefaultCalendar], "missing default calendar")
assert.NotNil(t, ecc.cache["S0"], "missing additional calendar")
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
ecc := &eventContainerCache{
userID: "test",
enumer: mockEventContainersEnumerator{containers: test.enumerateContainers()},
getter: mockEventContainerGetter{idToCalendar: getContainersByID()},
}
err := ecc.Populate(ctx, fault.New(true), "root", "root")
test.expectErr(t, err, clues.ToCore(err))
test.assertFunc(t, ecc)
})
}
}
// ---------------------------------------------------------------------------
// container resolver integration suite
// ---------------------------------------------------------------------------
type ContainerResolverIntgSuite struct {
tester.Suite
m365 its.M365IntgTestSetup
}
func TestContainerResolverIntgSuite(t *testing.T) {
suite.Run(t, &ContainerResolverIntgSuite{
Suite: tester.NewIntegrationSuite( Suite: tester.NewIntegrationSuite(
t, t,
[][]string{tconfig.M365AcctCredEnvs}), [][]string{tconfig.M365AcctCredEnvs}),
}) })
} }
func (suite *ContainerResolverIntgSuite) SetupSuite() { func (suite *ContainerResolverSuite) SetupSuite() {
suite.m365 = its.GetM365(suite.T()) t := suite.T()
a := tconfig.NewM365Account(t)
m365, err := a.M365Config()
require.NoError(t, err, clues.ToCore(err))
suite.credentials = m365
} }
func (suite *ContainerResolverIntgSuite) TestPopulate() { func (suite *ContainerResolverSuite) TestPopulate() {
ac, err := api.NewClient(
suite.credentials,
control.DefaultOptions(),
count.New())
require.NoError(suite.T(), err, clues.ToCore(err))
eventFunc := func(t *testing.T) graph.ContainerResolver { eventFunc := func(t *testing.T) graph.ContainerResolver {
return &eventContainerCache{ return &eventContainerCache{
userID: tconfig.M365UserID(t), userID: tconfig.M365UserID(t),
enumer: suite.m365.AC.Events(), enumer: ac.Events(),
getter: suite.m365.AC.Events(), getter: ac.Events(),
} }
} }
contactFunc := func(t *testing.T) graph.ContainerResolver { contactFunc := func(t *testing.T) graph.ContainerResolver {
return &contactContainerCache{ return &contactContainerCache{
userID: tconfig.M365UserID(t), userID: tconfig.M365UserID(t),
enumer: suite.m365.AC.Contacts(), enumer: ac.Contacts(),
getter: suite.m365.AC.Contacts(), getter: ac.Contacts(),
} }
} }

View File

@ -1,13 +1,6 @@
package exchange package exchange
import ( import (
"errors"
"net/http"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/services/m365/api" "github.com/alcionai/corso/src/pkg/services/m365/api"
"github.com/alcionai/corso/src/pkg/services/m365/api/graph" "github.com/alcionai/corso/src/pkg/services/m365/api/graph"
) )
@ -59,32 +52,3 @@ func (h eventBackupHandler) NewContainerCache(
getter: h.ac, getter: h.ac,
} }
} }
// todo: this could be further improved buy specifying the call source and matching that
// with the expected error. Might be necessary if we use this for more than one error.
// But since we only call this in a single place at this time, that additional guard isn't
// built into the func.
func (h eventBackupHandler) CanSkipItemFailure(
err error,
resourceID string,
opts control.Options,
) (fault.SkipCause, bool) {
if err == nil {
return "", false
}
// this is a bit overly cautious. we do know that we get 503s with empty response bodies
// due to fauilures when getting too many instances. We don't know for sure if we get
// generic, well formed 503s. But since we're working with specific resources and item
// IDs in the first place, that extra caution will help make sure an unexpected error dosn't
// slip through the cracks on us.
if !errors.Is(err, graph.ErrServiceUnavailableEmptyResp) &&
!clues.HasLabel(err, graph.LabelStatus(http.StatusServiceUnavailable)) {
return "", false
}
_, ok := opts.SkipEventsOnInstance503ForResources[resourceID]
// strict equals required here. ids are case sensitive.
return fault.SkipKnownEventInstance503s, ok
}

View File

@ -1,112 +0,0 @@
package exchange
import (
"net/http"
"testing"
"github.com/alcionai/clues"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/services/m365/api"
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
)
type EventsBackupHandlerUnitSuite struct {
tester.Suite
}
func TestEventsBackupHandlerUnitSuite(t *testing.T) {
suite.Run(t, &EventsBackupHandlerUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *EventsBackupHandlerUnitSuite) TestHandler_CanSkipItemFailure() {
resourceID := uuid.NewString()
table := []struct {
name string
err error
opts control.Options
expect assert.BoolAssertionFunc
expectCause fault.SkipCause
}{
{
name: "no config",
err: graph.ErrServiceUnavailableEmptyResp,
opts: control.Options{},
expect: assert.False,
expectCause: fault.SkipKnownEventInstance503s,
},
{
name: "empty skip on 503",
err: graph.ErrServiceUnavailableEmptyResp,
opts: control.Options{
SkipEventsOnInstance503ForResources: map[string]struct{}{},
},
expect: assert.False,
expectCause: fault.SkipKnownEventInstance503s,
},
{
name: "nil error",
err: nil,
opts: control.Options{
SkipEventsOnInstance503ForResources: map[string]struct{}{
resourceID: {},
},
},
expect: assert.False,
},
{
name: "non-matching resource",
err: graph.ErrServiceUnavailableEmptyResp,
opts: control.Options{
SkipEventsOnInstance503ForResources: map[string]struct{}{
"foo": {},
},
},
expect: assert.False,
expectCause: fault.SkipKnownEventInstance503s,
},
{
name: "match on instance 503 empty resp",
err: graph.ErrServiceUnavailableEmptyResp,
opts: control.Options{
SkipEventsOnInstance503ForResources: map[string]struct{}{
resourceID: {},
},
},
expect: assert.True,
expectCause: fault.SkipKnownEventInstance503s,
},
{
name: "match on instance 503",
err: clues.New("arbitrary error").
Label(graph.LabelStatus(http.StatusServiceUnavailable)),
opts: control.Options{
SkipEventsOnInstance503ForResources: map[string]struct{}{
resourceID: {},
},
},
expect: assert.True,
expectCause: fault.SkipKnownEventInstance503s,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
h := newEventBackupHandler(api.Client{})
cause, result := h.CanSkipItemFailure(
test.err,
resourceID,
test.opts)
test.expect(t, result)
assert.Equal(t, test.expectCause, cause)
})
}
}

View File

@ -2,7 +2,6 @@ package exchange
import ( import (
"context" "context"
"strings"
"time" "time"
"github.com/alcionai/clues" "github.com/alcionai/clues"
@ -61,16 +60,6 @@ func (ecc *eventContainerCache) populateEventRoot(ctx context.Context) error {
return nil return nil
} }
func isSharedCalendar(defaultCalendarOwner string, c models.Calendarable) bool {
// If we can't determine the owner, assume the calendar is owned by the
// user.
if len(defaultCalendarOwner) == 0 || c.GetOwner() == nil {
return false
}
return !strings.EqualFold(defaultCalendarOwner, ptr.Val(c.GetOwner().GetAddress()))
}
// Populate utility function for populating eventCalendarCache. // Populate utility function for populating eventCalendarCache.
// Executes 1 additional Graph Query // Executes 1 additional Graph Query
// @param baseID: ignored. Present to conform to interface // @param baseID: ignored. Present to conform to interface
@ -100,39 +89,11 @@ func (ecc *eventContainerCache) Populate(
return clues.WrapWC(ctx, err, "enumerating containers") return clues.WrapWC(ctx, err, "enumerating containers")
} }
var defaultCalendarOwner string
// Determine the owner for the default calendar. We'll use this to detect and
// skip shared calendars that are not owned by this user.
for _, c := range containers {
if ptr.Val(c.GetIsDefaultCalendar()) && c.GetOwner() != nil {
defaultCalendarOwner = ptr.Val(c.GetOwner().GetAddress())
ctx = clues.Add(ctx, "default_calendar_owner", defaultCalendarOwner)
break
}
}
for _, c := range containers { for _, c := range containers {
if el.Failure() != nil { if el.Failure() != nil {
return el.Failure() return el.Failure()
} }
// Skip shared calendars if we have enough information to determine the owner
if isSharedCalendar(defaultCalendarOwner, c) {
var ownerEmail string
if c.GetOwner() != nil {
ownerEmail = ptr.Val(c.GetOwner().GetAddress())
}
logger.Ctx(ctx).Infow(
"skipping shared calendar",
"name", ptr.Val(c.GetName()),
"owner", ownerEmail)
continue
}
cacheFolder := graph.NewCacheFolder( cacheFolder := graph.NewCacheFolder(
api.CalendarDisplayable{Calendarable: c}, api.CalendarDisplayable{Calendarable: c},
path.Builder{}.Append(ptr.Val(c.GetId())), path.Builder{}.Append(ptr.Val(c.GetId())),
@ -140,8 +101,9 @@ func (ecc *eventContainerCache) Populate(
err := ecc.addFolder(&cacheFolder) err := ecc.addFolder(&cacheFolder)
if err != nil { if err != nil {
err := clues.StackWC(ctx, err).Label(fault.LabelForceNoBackupCreation) errs.AddRecoverable(
errs.AddRecoverable(ctx, err) ctx,
graph.Stack(ctx, err).Label(fault.LabelForceNoBackupCreation))
} }
} }

View File

@ -158,7 +158,7 @@ func restoreEvent(
item, err := er.PostItem(ctx, userID, destinationID, event) item, err := er.PostItem(ctx, userID, destinationID, event)
if err != nil { if err != nil {
return nil, clues.Wrap(err, "restoring event") return nil, graph.Wrap(ctx, err, "restoring event")
} }
// events have no PUT request, and PATCH could retain data that's not // events have no PUT request, and PATCH could retain data that's not
@ -169,7 +169,7 @@ func restoreEvent(
if shouldDeleteOriginal { if shouldDeleteOriginal {
err := er.DeleteItem(ctx, userID, collisionID) err := er.DeleteItem(ctx, userID, collisionID)
if err != nil && !errors.Is(err, core.ErrNotFound) { if err != nil && !errors.Is(err, core.ErrNotFound) {
return nil, clues.Wrap(err, "deleting colliding event") return nil, graph.Wrap(ctx, err, "deleting colliding event")
} }
} }

View File

@ -13,7 +13,6 @@ import (
"github.com/alcionai/corso/src/internal/m365/service/exchange/mock" "github.com/alcionai/corso/src/internal/m365/service/exchange/mock"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/control/testdata" "github.com/alcionai/corso/src/pkg/control/testdata"
@ -102,7 +101,7 @@ func (m *eventRestoreMock) PatchItem(
type EventsRestoreIntgSuite struct { type EventsRestoreIntgSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup its intgTesterSetup
} }
func TestEventsRestoreIntgSuite(t *testing.T) { func TestEventsRestoreIntgSuite(t *testing.T) {
@ -114,17 +113,17 @@ func TestEventsRestoreIntgSuite(t *testing.T) {
} }
func (suite *EventsRestoreIntgSuite) SetupSuite() { func (suite *EventsRestoreIntgSuite) SetupSuite() {
suite.m365 = its.GetM365(suite.T()) suite.its = newIntegrationTesterSetup(suite.T())
} }
// Testing to ensure that cache system works for in multiple different environments // Testing to ensure that cache system works for in multiple different environments
func (suite *EventsRestoreIntgSuite) TestCreateContainerDestination() { func (suite *EventsRestoreIntgSuite) TestCreateContainerDestination() {
runCreateDestinationTest( runCreateDestinationTest(
suite.T(), suite.T(),
newEventRestoreHandler(suite.m365.AC), newEventRestoreHandler(suite.its.ac),
path.EventsCategory, path.EventsCategory,
suite.m365.TenantID, suite.its.creds.AzureTenantID,
suite.m365.User.ID, suite.its.userID,
testdata.DefaultRestoreConfig("").Location, testdata.DefaultRestoreConfig("").Location,
[]string{"Durmstrang"}, []string{"Durmstrang"},
[]string{"Beauxbatons"}) []string{"Beauxbatons"})
@ -265,7 +264,7 @@ func (suite *EventsRestoreIntgSuite) TestRestoreEvent() {
ctx, ctx,
test.apiMock, test.apiMock,
body, body,
suite.m365.User.ID, suite.its.userID,
"destination", "destination",
test.collisionMap, test.collisionMap,
test.onCollision, test.onCollision,

View File

@ -26,8 +26,6 @@ type backupHandler interface {
previewIncludeContainers() []string previewIncludeContainers() []string
previewExcludeContainers() []string previewExcludeContainers() []string
NewContainerCache(userID string) (string, graph.ContainerResolver) NewContainerCache(userID string) (string, graph.ContainerResolver)
canSkipItemFailurer
} }
type addedAndRemovedItemGetter interface { type addedAndRemovedItemGetter interface {
@ -59,14 +57,6 @@ func BackupHandlers(ac api.Client) map[path.CategoryType]backupHandler {
} }
} }
type canSkipItemFailurer interface {
CanSkipItemFailure(
err error,
resourceID string,
opts control.Options,
) (fault.SkipCause, bool)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// restore // restore
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -0,0 +1,44 @@
package exchange
import (
"testing"
"github.com/alcionai/clues"
"github.com/stretchr/testify/require"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/count"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
type intgTesterSetup struct {
ac api.Client
creds account.M365Config
userID string
}
func newIntegrationTesterSetup(t *testing.T) intgTesterSetup {
its := intgTesterSetup{}
ctx, flush := tester.NewContext(t)
defer flush()
a := tconfig.NewM365Account(t)
creds, err := a.M365Config()
require.NoError(t, err, clues.ToCore(err))
its.creds = creds
its.ac, err = api.NewClient(
creds,
control.DefaultOptions(),
count.New())
require.NoError(t, err, clues.ToCore(err))
its.userID = tconfig.GetM365UserID(ctx)
return its
}

View File

@ -1,8 +1,6 @@
package exchange package exchange
import ( import (
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/services/m365/api" "github.com/alcionai/corso/src/pkg/services/m365/api"
"github.com/alcionai/corso/src/pkg/services/m365/api/graph" "github.com/alcionai/corso/src/pkg/services/m365/api/graph"
) )
@ -59,11 +57,3 @@ func (h mailBackupHandler) NewContainerCache(
getter: h.ac, getter: h.ac,
} }
} }
func (h mailBackupHandler) CanSkipItemFailure(
err error,
resourceID string,
opts control.Options,
) (fault.SkipCause, bool) {
return "", false
}

View File

@ -1,83 +0,0 @@
package exchange
import (
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
type MailBackupHandlerUnitSuite struct {
tester.Suite
}
func TestMailBackupHandlerUnitSuite(t *testing.T) {
suite.Run(t, &MailBackupHandlerUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *MailBackupHandlerUnitSuite) TestHandler_CanSkipItemFailure() {
resourceID := uuid.NewString()
table := []struct {
name string
err error
opts control.Options
expect assert.BoolAssertionFunc
expectCause fault.SkipCause
}{
{
name: "no config",
err: assert.AnError,
opts: control.Options{},
expect: assert.False,
},
{
name: "false when map is empty",
err: assert.AnError,
opts: control.Options{
SkipEventsOnInstance503ForResources: map[string]struct{}{},
},
expect: assert.False,
},
{
name: "false on nil error",
err: nil,
opts: control.Options{
SkipEventsOnInstance503ForResources: map[string]struct{}{
resourceID: {},
},
},
expect: assert.False,
},
{
name: "false even if resource matches",
err: assert.AnError,
opts: control.Options{
SkipEventsOnInstance503ForResources: map[string]struct{}{
resourceID: {},
},
},
expect: assert.False,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
h := newMailBackupHandler(api.Client{})
cause, result := h.CanSkipItemFailure(
test.err,
resourceID,
test.opts)
test.expect(t, result)
assert.Equal(t, test.expectCause, cause)
})
}
}

View File

@ -128,8 +128,9 @@ func (mc *mailContainerCache) Populate(
err := mc.addFolder(&cacheFolder) err := mc.addFolder(&cacheFolder)
if err != nil { if err != nil {
err = clues.StackWC(ctx, err).Label(fault.LabelForceNoBackupCreation) errs.AddRecoverable(
errs.AddRecoverable(ctx, err) ctx,
graph.Stack(ctx, err).Label(fault.LabelForceNoBackupCreation))
} }
} }

View File

@ -10,8 +10,10 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/count"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/services/m365/api" "github.com/alcionai/corso/src/pkg/services/m365/api"
) )
@ -28,24 +30,30 @@ const (
expectedFolderPath = "toplevel/subFolder/subsubfolder" expectedFolderPath = "toplevel/subFolder/subsubfolder"
) )
type MailFolderCacheIntgSuite struct { type MailFolderCacheIntegrationSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup credentials account.M365Config
} }
func TestMailFolderCacheIntegrationSuite(t *testing.T) { func TestMailFolderCacheIntegrationSuite(t *testing.T) {
suite.Run(t, &MailFolderCacheIntgSuite{ suite.Run(t, &MailFolderCacheIntegrationSuite{
Suite: tester.NewIntegrationSuite( Suite: tester.NewIntegrationSuite(
t, t,
[][]string{tconfig.M365AcctCredEnvs}), [][]string{tconfig.M365AcctCredEnvs}),
}) })
} }
func (suite *MailFolderCacheIntgSuite) SetupSuite() { func (suite *MailFolderCacheIntegrationSuite) SetupSuite() {
suite.m365 = its.GetM365(suite.T()) t := suite.T()
a := tconfig.NewM365Account(t)
m365, err := a.M365Config()
require.NoError(t, err, clues.ToCore(err))
suite.credentials = m365
} }
func (suite *MailFolderCacheIntgSuite) TestDeltaFetch() { func (suite *MailFolderCacheIntegrationSuite) TestDeltaFetch() {
suite.T().Skipf("Test depends on hardcoded folder names. Skipping till that is fixed") suite.T().Skipf("Test depends on hardcoded folder names. Skipping till that is fixed")
tests := []struct { tests := []struct {
@ -67,6 +75,7 @@ func (suite *MailFolderCacheIntgSuite) TestDeltaFetch() {
path: []string{"some", "leading", "path"}, path: []string{"some", "leading", "path"},
}, },
} }
userID := tconfig.M365UserID(suite.T())
for _, test := range tests { for _, test := range tests {
suite.Run(test.name, func() { suite.Run(test.name, func() {
@ -75,15 +84,21 @@ func (suite *MailFolderCacheIntgSuite) TestDeltaFetch() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
acm := suite.m365.AC.Mail() ac, err := api.NewClient(
suite.credentials,
control.DefaultOptions(),
count.New())
require.NoError(t, err, clues.ToCore(err))
acm := ac.Mail()
mfc := mailContainerCache{ mfc := mailContainerCache{
userID: suite.m365.User.ID, userID: userID,
enumer: acm, enumer: acm,
getter: acm, getter: acm,
} }
err := mfc.Populate(ctx, fault.New(true), test.root, test.path...) err = mfc.Populate(ctx, fault.New(true), test.root, test.path...)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
p, l, err := mfc.IDToPath(ctx, testFolderID) p, l, err := mfc.IDToPath(ctx, testFolderID)

View File

@ -3,7 +3,6 @@ package exchange
import ( import (
"context" "context"
"errors" "errors"
"regexp"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
@ -148,15 +147,13 @@ func restoreMail(
msg = setMessageSVEPs(toMessage(msg)) msg = setMessageSVEPs(toMessage(msg))
setReplyTos(msg)
attachments := msg.GetAttachments() attachments := msg.GetAttachments()
// Item.Attachments --> HasAttachments doesn't always have a value populated when deserialized // Item.Attachments --> HasAttachments doesn't always have a value populated when deserialized
msg.SetAttachments([]models.Attachmentable{}) msg.SetAttachments([]models.Attachmentable{})
item, err := mr.PostItem(ctx, userID, destinationID, msg) item, err := mr.PostItem(ctx, userID, destinationID, msg)
if err != nil { if err != nil {
return nil, clues.Wrap(err, "restoring mail message") return nil, graph.Wrap(ctx, err, "restoring mail message")
} }
// mails have no PUT request, and PATCH could retain data that's not // mails have no PUT request, and PATCH could retain data that's not
@ -167,7 +164,7 @@ func restoreMail(
if shouldDeleteOriginal { if shouldDeleteOriginal {
err := mr.DeleteItem(ctx, userID, collisionID) err := mr.DeleteItem(ctx, userID, collisionID)
if err != nil && !errors.Is(err, core.ErrNotFound) { if err != nil && !errors.Is(err, core.ErrNotFound) {
return nil, clues.Wrap(err, "deleting colliding mail message") return nil, graph.Wrap(ctx, err, "deleting colliding mail message")
} }
} }
@ -232,38 +229,6 @@ func setMessageSVEPs(msg models.Messageable) models.Messageable {
return msg return msg
} }
func setReplyTos(msg models.Messageable) {
var (
replyTos = msg.GetReplyTo()
emailAddress models.EmailAddressable
name, address string
sanitizedReplyTos = make([]models.Recipientable, 0)
)
if len(replyTos) == 0 {
return
}
for _, replyTo := range replyTos {
emailAddress = replyTo.GetEmailAddress()
address = ptr.Val(emailAddress.GetAddress())
name = ptr.Val(emailAddress.GetName())
if isValidEmail(address) || isValidDN(address) {
newEmailAddress := models.NewEmailAddress()
newEmailAddress.SetAddress(ptr.To(address))
newEmailAddress.SetName(ptr.To(name))
sanitizedReplyTo := models.NewRecipient()
sanitizedReplyTo.SetEmailAddress(newEmailAddress)
sanitizedReplyTos = append(sanitizedReplyTos, sanitizedReplyTo)
}
}
msg.SetReplyTo(sanitizedReplyTos)
}
func (h mailRestoreHandler) GetItemsInContainerByCollisionKey( func (h mailRestoreHandler) GetItemsInContainerByCollisionKey(
ctx context.Context, ctx context.Context,
userID, containerID string, userID, containerID string,
@ -275,24 +240,3 @@ func (h mailRestoreHandler) GetItemsInContainerByCollisionKey(
return m, nil return m, nil
} }
// [TODO]relocate to a common place
func isValidEmail(email string) bool {
emailRegex := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
r := regexp.MustCompile(emailRegex)
return r.MatchString(email)
}
// isValidDN check if given string's format matches that of a MSFT Distinguished Name
// This regular expression matches strings that start with /o=,
// followed by any characters except /,
// then /ou=, followed by any characters except /,
// then /cn=, followed by any characters except /,
// then /cn= followed by a 32-character hexadecimal string followed by - and any additional characters.
func isValidDN(dn string) bool {
dnRegex := `^/o=[^/]+/ou=[^/]+/cn=[^/]+/cn=[a-fA-F0-9]{32}-[a-zA-Z0-9-]+$`
r := regexp.MustCompile(dnRegex)
return r.MatchString(dn)
}

View File

@ -11,10 +11,8 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/m365/service/exchange/mock" "github.com/alcionai/corso/src/internal/m365/service/exchange/mock"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/control/testdata" "github.com/alcionai/corso/src/pkg/control/testdata"
@ -25,127 +23,6 @@ import (
"github.com/alcionai/corso/src/pkg/services/m365/api" "github.com/alcionai/corso/src/pkg/services/m365/api"
) )
//nolint:lll
const TestDN = "/o=ExchangeLabs/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=4eca0d46a2324036b0b326dc58cfc802-user"
type RestoreMailUnitSuite struct {
tester.Suite
}
func TestRestoreMailUnitSuite(t *testing.T) {
suite.Run(t, &RestoreMailUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *RestoreMailUnitSuite) TestIsValidEmail() {
table := []struct {
name string
email string
check assert.BoolAssertionFunc
}{
{
name: "valid email",
email: "foo@bar.com",
check: assert.True,
},
{
name: "invalid email, missing domain",
email: "foo.com",
check: assert.False,
},
{
name: "invalid email, random uuid",
email: "12345678-abcd-90ef-88f8-2d95ef12fb66",
check: assert.False,
},
{
name: "empty email",
email: "",
check: assert.False,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
result := isValidEmail(test.email)
test.check(t, result)
})
}
}
func (suite *RestoreMailUnitSuite) TestIsValidDN() {
table := []struct {
name string
dn string
check assert.BoolAssertionFunc
}{
{
name: "valid DN",
dn: TestDN,
check: assert.True,
},
{
name: "invalid DN",
dn: "random string",
check: assert.False,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
result := isValidDN(test.dn)
test.check(t, result)
})
}
}
func (suite *RestoreMailUnitSuite) TestSetReplyTos() {
t := suite.T()
replyTos := make([]models.Recipientable, 0)
emailAddresses := map[string]string{
"foo.bar": "foo@bar.com",
"foo.com": "foo.com",
"empty": "",
"dn": TestDN,
}
validEmailAddresses := map[string]string{
"foo.bar": "foo@bar.com",
"dn": TestDN,
}
for k, v := range emailAddresses {
emailAddress := models.NewEmailAddress()
emailAddress.SetAddress(ptr.To(v))
emailAddress.SetName(ptr.To(k))
replyTo := models.NewRecipient()
replyTo.SetEmailAddress(emailAddress)
replyTos = append(replyTos, replyTo)
}
mailMessage := models.NewMessage()
mailMessage.SetReplyTo(replyTos)
setReplyTos(mailMessage)
sanitizedReplyTos := mailMessage.GetReplyTo()
require.Len(t, sanitizedReplyTos, len(validEmailAddresses))
for _, sanitizedReplyTo := range sanitizedReplyTos {
emailAddress := sanitizedReplyTo.GetEmailAddress()
assert.Contains(t, validEmailAddresses, ptr.Val(emailAddress.GetName()))
assert.Equal(t, validEmailAddresses[ptr.Val(emailAddress.GetName())], ptr.Val(emailAddress.GetAddress()))
}
}
var _ mailRestorer = &mailRestoreMock{} var _ mailRestorer = &mailRestoreMock{}
type mailRestoreMock struct { type mailRestoreMock struct {
@ -195,7 +72,7 @@ func (m *mailRestoreMock) PostLargeAttachment(
type MailRestoreIntgSuite struct { type MailRestoreIntgSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup its intgTesterSetup
} }
func TestMailRestoreIntgSuite(t *testing.T) { func TestMailRestoreIntgSuite(t *testing.T) {
@ -207,16 +84,16 @@ func TestMailRestoreIntgSuite(t *testing.T) {
} }
func (suite *MailRestoreIntgSuite) SetupSuite() { func (suite *MailRestoreIntgSuite) SetupSuite() {
suite.m365 = its.GetM365(suite.T()) suite.its = newIntegrationTesterSetup(suite.T())
} }
func (suite *MailRestoreIntgSuite) TestCreateContainerDestination() { func (suite *MailRestoreIntgSuite) TestCreateContainerDestination() {
runCreateDestinationTest( runCreateDestinationTest(
suite.T(), suite.T(),
newMailRestoreHandler(suite.m365.AC), newMailRestoreHandler(suite.its.ac),
path.EmailCategory, path.EmailCategory,
suite.m365.TenantID, suite.its.creds.AzureTenantID,
suite.m365.User.ID, suite.its.userID,
testdata.DefaultRestoreConfig("").Location, testdata.DefaultRestoreConfig("").Location,
[]string{"Griffindor", "Croix"}, []string{"Griffindor", "Croix"},
[]string{"Griffindor", "Felicius"}) []string{"Griffindor", "Felicius"})
@ -357,7 +234,7 @@ func (suite *MailRestoreIntgSuite) TestRestoreMail() {
ctx, ctx,
test.apiMock, test.apiMock,
body, body,
suite.m365.User.ID, suite.its.userID,
"destination", "destination",
test.collisionMap, test.collisionMap,
test.onCollision, test.onCollision,

View File

@ -6,15 +6,10 @@ import (
"github.com/microsoft/kiota-abstractions-go/serialization" "github.com/microsoft/kiota-abstractions-go/serialization"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/services/m365/api" "github.com/alcionai/corso/src/pkg/services/m365/api"
) )
// ---------------------------------------------------------------------------
// get and serialize item mock
// ---------------------------------------------------------------------------
type ItemGetSerialize struct { type ItemGetSerialize struct {
GetData serialization.Parsable GetData serialization.Parsable
GetCount int GetCount int
@ -49,23 +44,3 @@ func (m *ItemGetSerialize) Serialize(
func DefaultItemGetSerialize() *ItemGetSerialize { func DefaultItemGetSerialize() *ItemGetSerialize {
return &ItemGetSerialize{} return &ItemGetSerialize{}
} }
// ---------------------------------------------------------------------------
// can skip item failure mock
// ---------------------------------------------------------------------------
type canSkipFailChecker struct {
canSkip bool
}
func (m canSkipFailChecker) CanSkipItemFailure(
err error,
resourceID string,
opts control.Options,
) (fault.SkipCause, bool) {
return fault.SkipCause("testing"), m.canSkip
}
func NeverCanSkipFailChecker() *canSkipFailChecker {
return &canSkipFailChecker{}
}

View File

@ -12,8 +12,8 @@ import (
"github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/ptr"
exchMock "github.com/alcionai/corso/src/internal/m365/service/exchange/mock" exchMock "github.com/alcionai/corso/src/internal/m365/service/exchange/mock"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/control/testdata" "github.com/alcionai/corso/src/pkg/control/testdata"
"github.com/alcionai/corso/src/pkg/count" "github.com/alcionai/corso/src/pkg/count"
@ -24,7 +24,8 @@ import (
type RestoreIntgSuite struct { type RestoreIntgSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup credentials account.M365Config
ac api.Client
} }
func TestRestoreIntgSuite(t *testing.T) { func TestRestoreIntgSuite(t *testing.T) {
@ -36,7 +37,18 @@ func TestRestoreIntgSuite(t *testing.T) {
} }
func (suite *RestoreIntgSuite) SetupSuite() { func (suite *RestoreIntgSuite) SetupSuite() {
suite.m365 = its.GetM365(suite.T()) t := suite.T()
a := tconfig.NewM365Account(t)
m365, err := a.M365Config()
require.NoError(t, err, clues.ToCore(err))
suite.credentials = m365
suite.ac, err = api.NewClient(
m365,
control.DefaultOptions(),
count.New())
require.NoError(t, err, clues.ToCore(err))
} }
// TestRestoreContact ensures contact object can be created, placed into // TestRestoreContact ensures contact object can be created, placed into
@ -48,26 +60,26 @@ func (suite *RestoreIntgSuite) TestRestoreContact() {
defer flush() defer flush()
var ( var (
userID = tconfig.M365UserID(t)
folderName = testdata.DefaultRestoreConfig("contact").Location folderName = testdata.DefaultRestoreConfig("contact").Location
handler = newContactRestoreHandler(suite.m365.AC) handler = newContactRestoreHandler(suite.ac)
) )
aFolder, err := handler.ac.CreateContainer(ctx, suite.m365.User.ID, "", folderName) aFolder, err := handler.ac.CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
folderID := ptr.Val(aFolder.GetId()) folderID := ptr.Val(aFolder.GetId())
defer func() { defer func() {
// Remove the folder containing contact prior to exiting test // Remove the folder containing contact prior to exiting test
err = suite.m365.AC.Contacts().DeleteContainer(ctx, suite.m365.User.ID, folderID) err = suite.ac.Contacts().DeleteContainer(ctx, userID, folderID)
assert.NoError(t, err, clues.ToCore(err)) assert.NoError(t, err, clues.ToCore(err))
}() }()
info, err := handler.restore( info, err := handler.restore(
ctx, ctx,
exchMock.ContactBytes("Corso TestContact"), exchMock.ContactBytes("Corso TestContact"),
suite.m365.User.ID, userID, folderID,
folderID,
nil, nil,
control.Copy, control.Copy,
fault.New(true), fault.New(true),
@ -85,18 +97,19 @@ func (suite *RestoreIntgSuite) TestRestoreEvent() {
defer flush() defer flush()
var ( var (
userID = tconfig.M365UserID(t)
subject = testdata.DefaultRestoreConfig("event").Location subject = testdata.DefaultRestoreConfig("event").Location
handler = newEventRestoreHandler(suite.m365.AC) handler = newEventRestoreHandler(suite.ac)
) )
calendar, err := handler.ac.CreateContainer(ctx, suite.m365.User.ID, "", subject) calendar, err := handler.ac.CreateContainer(ctx, userID, "", subject)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
calendarID := ptr.Val(calendar.GetId()) calendarID := ptr.Val(calendar.GetId())
defer func() { defer func() {
// Removes calendar containing events created during the test // Removes calendar containing events created during the test
err = suite.m365.AC.Events().DeleteContainer(ctx, suite.m365.User.ID, calendarID) err = suite.ac.Events().DeleteContainer(ctx, userID, calendarID)
assert.NoError(t, err, clues.ToCore(err)) assert.NoError(t, err, clues.ToCore(err))
}() }()
@ -141,8 +154,7 @@ func (suite *RestoreIntgSuite) TestRestoreEvent() {
info, err := handler.restore( info, err := handler.restore(
ctx, ctx,
test.bytes, test.bytes,
suite.m365.User.ID, userID, calendarID,
calendarID,
nil, nil,
control.Copy, control.Copy,
fault.New(true), fault.New(true),
@ -156,7 +168,10 @@ func (suite *RestoreIntgSuite) TestRestoreEvent() {
// TestRestoreExchangeObject verifies path.Category usage for restored objects // TestRestoreExchangeObject verifies path.Category usage for restored objects
func (suite *RestoreIntgSuite) TestRestoreExchangeObject() { func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
t := suite.T() t := suite.T()
handlers := RestoreHandlers(suite.m365.AC)
handlers := RestoreHandlers(suite.ac)
userID := tconfig.M365UserID(suite.T())
tests := []struct { tests := []struct {
name string name string
@ -171,7 +186,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("mailobj").Location folderName := testdata.DefaultRestoreConfig("mailobj").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, suite.m365.User.ID, "", folderName) CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -184,7 +199,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("mailwattch").Location folderName := testdata.DefaultRestoreConfig("mailwattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, suite.m365.User.ID, "", folderName) CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -197,7 +212,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("eventwattch").Location folderName := testdata.DefaultRestoreConfig("eventwattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, suite.m365.User.ID, "", folderName) CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -210,7 +225,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("mailitemattch").Location folderName := testdata.DefaultRestoreConfig("mailitemattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, suite.m365.User.ID, "", folderName) CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -225,7 +240,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("mailbasicattch").Location folderName := testdata.DefaultRestoreConfig("mailbasicattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, suite.m365.User.ID, "", folderName) CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -240,7 +255,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("mailnestattch").Location folderName := testdata.DefaultRestoreConfig("mailnestattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, suite.m365.User.ID, "", folderName) CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -255,7 +270,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("mailcontactattch").Location folderName := testdata.DefaultRestoreConfig("mailcontactattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, suite.m365.User.ID, "", folderName) CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -268,7 +283,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("nestedattch").Location folderName := testdata.DefaultRestoreConfig("nestedattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, suite.m365.User.ID, "", folderName) CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -281,7 +296,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("maillargeattch").Location folderName := testdata.DefaultRestoreConfig("maillargeattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, suite.m365.User.ID, "", folderName) CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -294,7 +309,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("mailtwoattch").Location folderName := testdata.DefaultRestoreConfig("mailtwoattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, suite.m365.User.ID, "", folderName) CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -307,7 +322,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("mailrefattch").Location folderName := testdata.DefaultRestoreConfig("mailrefattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, suite.m365.User.ID, "", folderName) CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -320,7 +335,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("contact").Location folderName := testdata.DefaultRestoreConfig("contact").Location
folder, err := handlers[path.ContactsCategory]. folder, err := handlers[path.ContactsCategory].
CreateContainer(ctx, suite.m365.User.ID, "", folderName) CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -333,7 +348,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("event").Location folderName := testdata.DefaultRestoreConfig("event").Location
calendar, err := handlers[path.EventsCategory]. calendar, err := handlers[path.EventsCategory].
CreateContainer(ctx, suite.m365.User.ID, "", folderName) CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(calendar.GetId()) return ptr.Val(calendar.GetId())
@ -346,7 +361,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("eventobj").Location folderName := testdata.DefaultRestoreConfig("eventobj").Location
calendar, err := handlers[path.EventsCategory]. calendar, err := handlers[path.EventsCategory].
CreateContainer(ctx, suite.m365.User.ID, "", folderName) CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(calendar.GetId()) return ptr.Val(calendar.GetId())
@ -365,8 +380,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
info, err := handlers[test.category].restore( info, err := handlers[test.category].restore(
ctx, ctx,
test.bytes, test.bytes,
suite.m365.User.ID, userID, destination,
destination,
nil, nil,
control.Copy, control.Copy,
fault.New(true), fault.New(true),
@ -386,11 +400,12 @@ func (suite *RestoreIntgSuite) TestRestoreAndBackupEvent_recurringInstancesWithA
defer flush() defer flush()
var ( var (
userID = tconfig.M365UserID(t)
subject = testdata.DefaultRestoreConfig("event").Location subject = testdata.DefaultRestoreConfig("event").Location
handler = newEventRestoreHandler(suite.m365.AC) handler = newEventRestoreHandler(suite.ac)
) )
calendar, err := handler.ac.CreateContainer(ctx, suite.m365.User.ID, "", subject) calendar, err := handler.ac.CreateContainer(ctx, userID, "", subject)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
calendarID := ptr.Val(calendar.GetId()) calendarID := ptr.Val(calendar.GetId())
@ -399,8 +414,7 @@ func (suite *RestoreIntgSuite) TestRestoreAndBackupEvent_recurringInstancesWithA
info, err := handler.restore( info, err := handler.restore(
ctx, ctx,
bytes, bytes,
suite.m365.User.ID, userID, calendarID,
calendarID,
nil, nil,
control.Copy, control.Copy,
fault.New(true), fault.New(true),
@ -411,7 +425,7 @@ func (suite *RestoreIntgSuite) TestRestoreAndBackupEvent_recurringInstancesWithA
ec, err := handler.ac.Stable. ec, err := handler.ac.Stable.
Client(). Client().
Users(). Users().
ByUserId(suite.m365.User.ID). ByUserId(userID).
Calendars(). Calendars().
ByCalendarId(calendarID). ByCalendarId(calendarID).
Events(). Events().
@ -421,25 +435,17 @@ func (suite *RestoreIntgSuite) TestRestoreAndBackupEvent_recurringInstancesWithA
evts := ec.GetValue() evts := ec.GetValue()
assert.Len(t, evts, 1, "count of events") assert.Len(t, evts, 1, "count of events")
sp, info, err := suite.m365.AC.Events().GetItem( sp, info, err := suite.ac.Events().GetItem(ctx, userID, ptr.Val(evts[0].GetId()), fault.New(true))
ctx,
suite.m365.User.ID,
ptr.Val(evts[0].GetId()),
fault.New(true))
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
assert.NotNil(t, info, "event item info") assert.NotNil(t, info, "event item info")
body, err := suite.m365.AC.Events().Serialize( body, err := suite.ac.Events().Serialize(ctx, sp, userID, ptr.Val(evts[0].GetId()))
ctx,
sp,
suite.m365.User.ID,
ptr.Val(evts[0].GetId()))
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
event, err := api.BytesToEventable(body) event, err := api.BytesToEventable(body)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
assert.NotNil(t, event.GetRecurrence(), "recurrence")
assert.NotNil(t, event.GetRecurrence(), "recurrence")
eo := event.GetAdditionalData()["exceptionOccurrences"] eo := event.GetAdditionalData()["exceptionOccurrences"]
assert.NotNil(t, eo, "exceptionOccurrences") assert.NotNil(t, eo, "exceptionOccurrences")

View File

@ -105,17 +105,13 @@ func populateCollections[C graph.GetIDer, I groupsItemer](
// channel ID -> delta url or folder path lookups // channel ID -> delta url or folder path lookups
deltaURLs = map[string]string{} deltaURLs = map[string]string{}
currPaths = map[string]string{} currPaths = map[string]string{}
// copy of previousPaths. every channel present in the slice param
// gets removed from this map; the remaining channels at the end of
// the process have been deleted.
tombstones = makeTombstones(dps)
el = errs.Local() el = errs.Local()
) )
// Copy of previousPaths. Every container present in the slice param
// gets removed from this map; the remaining containers at the end of
// the process have been deleted.
tombstones, err := bh.makeTombstones(dps)
if err != nil {
return nil, clues.StackWC(ctx, err)
}
logger.Ctx(ctx).Infow("filling collections", "len_deltapaths", len(dps)) logger.Ctx(ctx).Infow("filling collections", "len_deltapaths", len(dps))
for _, c := range containers { for _, c := range containers {

View File

@ -18,7 +18,6 @@ import (
"github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/m365/support"
"github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/internal/operations/inject"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/internal/version" "github.com/alcionai/corso/src/internal/version"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
@ -40,24 +39,22 @@ import (
// mocks // mocks
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
var _ backupHandler[models.Channelable, models.ChatMessageable] = &mockChannelsBH{} var _ backupHandler[models.Channelable, models.ChatMessageable] = &mockBackupHandler{}
//lint:ignore U1000 false linter issue due to generics //lint:ignore U1000 false linter issue due to generics
type mockChannelsBH struct { type mockBackupHandler struct {
channels []models.Channelable channels []models.Channelable
conversations []models.Conversationable
messageIDs []string messageIDs []string
deletedMsgIDs []string deletedMsgIDs []string
messagesErr error messagesErr error
messages map[string]models.ChatMessageable messages map[string]models.ChatMessageable
posts map[string]models.Postable
info map[string]*details.GroupsInfo info map[string]*details.GroupsInfo
getMessageErr map[string]error getMessageErr map[string]error
doNotInclude bool doNotInclude bool
} }
//lint:ignore U1000 false linter issue due to generics //lint:ignore U1000 false linter issue due to generics
func (bh mockChannelsBH) augmentItemInfo( func (bh mockBackupHandler) augmentItemInfo(
*details.GroupsInfo, *details.GroupsInfo,
models.Channelable, models.Channelable,
) { ) {
@ -65,15 +62,15 @@ func (bh mockChannelsBH) augmentItemInfo(
} }
//lint:ignore U1000 false linter issue due to generics //lint:ignore U1000 false linter issue due to generics
func (bh mockChannelsBH) supportsItemMetadata() bool { func (bh mockBackupHandler) supportsItemMetadata() bool {
return false return false
} }
func (bh mockChannelsBH) canMakeDeltaQueries() bool { func (bh mockBackupHandler) canMakeDeltaQueries() bool {
return true return true
} }
func (bh mockChannelsBH) containers() []container[models.Channelable] { func (bh mockBackupHandler) containers() []container[models.Channelable] {
containers := make([]container[models.Channelable], 0, len(bh.channels)) containers := make([]container[models.Channelable], 0, len(bh.channels))
for _, ch := range bh.channels { for _, ch := range bh.channels {
@ -84,14 +81,14 @@ func (bh mockChannelsBH) containers() []container[models.Channelable] {
} }
//lint:ignore U1000 required for interface compliance //lint:ignore U1000 required for interface compliance
func (bh mockChannelsBH) getContainers( func (bh mockBackupHandler) getContainers(
context.Context, context.Context,
api.CallConfig, api.CallConfig,
) ([]container[models.Channelable], error) { ) ([]container[models.Channelable], error) {
return bh.containers(), nil return bh.containers(), nil
} }
func (bh mockChannelsBH) getContainerItemIDs( func (bh mockBackupHandler) getContainerItemIDs(
_ context.Context, _ context.Context,
_ path.Elements, _ path.Elements,
_ string, _ string,
@ -114,14 +111,14 @@ func (bh mockChannelsBH) getContainerItemIDs(
} }
//lint:ignore U1000 required for interface compliance //lint:ignore U1000 required for interface compliance
func (bh mockChannelsBH) includeContainer( func (bh mockBackupHandler) includeContainer(
models.Channelable, models.Channelable,
selectors.GroupsScope, selectors.GroupsScope,
) bool { ) bool {
return !bh.doNotInclude return !bh.doNotInclude
} }
func (bh mockChannelsBH) canonicalPath( func (bh mockBackupHandler) canonicalPath(
storageDirFolders path.Elements, storageDirFolders path.Elements,
tenantID string, tenantID string,
) (path.Path, error) { ) (path.Path, error) {
@ -136,7 +133,7 @@ func (bh mockChannelsBH) canonicalPath(
} }
//lint:ignore U1000 false linter issue due to generics //lint:ignore U1000 false linter issue due to generics
func (bh mockChannelsBH) getItem( func (bh mockBackupHandler) getItem(
_ context.Context, _ context.Context,
_ string, _ string,
_ path.Elements, _ path.Elements,
@ -145,20 +142,13 @@ func (bh mockChannelsBH) getItem(
return bh.messages[itemID], bh.info[itemID], bh.getMessageErr[itemID] return bh.messages[itemID], bh.info[itemID], bh.getMessageErr[itemID]
} }
func (bh mockChannelsBH) getItemMetadata( func (bh mockBackupHandler) getItemMetadata(
_ context.Context, _ context.Context,
_ models.Channelable, _ models.Channelable,
) (io.ReadCloser, int, error) { ) (io.ReadCloser, int, error) {
return nil, 0, errMetadataFilesNotSupported return nil, 0, errMetadataFilesNotSupported
} }
//lint:ignore U1000 false linter issue due to generics
func (bh mockChannelsBH) makeTombstones(
dps metadata.DeltaPaths,
) (map[string]string, error) {
return makeTombstones(dps), nil
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Unit Suite // Unit Suite
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -179,10 +169,6 @@ func (suite *BackupUnitSuite) SetupSuite() {
suite.creds = m365 suite.creds = m365
} }
// ---------------------------------------------------------------------------
// Channels tests
// ---------------------------------------------------------------------------
func (suite *BackupUnitSuite) TestPopulateCollections() { func (suite *BackupUnitSuite) TestPopulateCollections() {
var ( var (
qp = graph.QueryParams{ qp = graph.QueryParams{
@ -195,7 +181,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
table := []struct { table := []struct {
name string name string
mock mockChannelsBH mock mockBackupHandler
expectErr require.ErrorAssertionFunc expectErr require.ErrorAssertionFunc
expectColls int expectColls int
expectNewColls int expectNewColls int
@ -203,7 +189,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
}{ }{
{ {
name: "happy path, one container", name: "happy path, one container",
mock: mockChannelsBH{ mock: mockBackupHandler{
channels: testdata.StubChannels("one"), channels: testdata.StubChannels("one"),
messageIDs: []string{"msg-one"}, messageIDs: []string{"msg-one"},
}, },
@ -214,7 +200,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
}, },
{ {
name: "happy path, one container, only deleted messages", name: "happy path, one container, only deleted messages",
mock: mockChannelsBH{ mock: mockBackupHandler{
channels: testdata.StubChannels("one"), channels: testdata.StubChannels("one"),
deletedMsgIDs: []string{"msg-one"}, deletedMsgIDs: []string{"msg-one"},
}, },
@ -225,7 +211,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
}, },
{ {
name: "happy path, many containers", name: "happy path, many containers",
mock: mockChannelsBH{ mock: mockBackupHandler{
channels: testdata.StubChannels("one", "two"), channels: testdata.StubChannels("one", "two"),
messageIDs: []string{"msg-one"}, messageIDs: []string{"msg-one"},
}, },
@ -236,7 +222,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
}, },
{ {
name: "no containers pass scope", name: "no containers pass scope",
mock: mockChannelsBH{ mock: mockBackupHandler{
channels: testdata.StubChannels("one"), channels: testdata.StubChannels("one"),
doNotInclude: true, doNotInclude: true,
}, },
@ -247,7 +233,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
}, },
{ {
name: "no channels", name: "no channels",
mock: mockChannelsBH{}, mock: mockBackupHandler{},
expectErr: require.NoError, expectErr: require.NoError,
expectColls: 1, expectColls: 1,
expectNewColls: 0, expectNewColls: 0,
@ -255,7 +241,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
}, },
{ {
name: "no channel messages", name: "no channel messages",
mock: mockChannelsBH{ mock: mockBackupHandler{
channels: testdata.StubChannels("one"), channels: testdata.StubChannels("one"),
}, },
expectErr: require.NoError, expectErr: require.NoError,
@ -265,7 +251,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
}, },
{ {
name: "err: deleted in flight", name: "err: deleted in flight",
mock: mockChannelsBH{ mock: mockBackupHandler{
channels: testdata.StubChannels("one"), channels: testdata.StubChannels("one"),
messagesErr: core.ErrNotFound, messagesErr: core.ErrNotFound,
}, },
@ -276,7 +262,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
}, },
{ {
name: "err: other error", name: "err: other error",
mock: mockChannelsBH{ mock: mockBackupHandler{
channels: testdata.StubChannels("one"), channels: testdata.StubChannels("one"),
messagesErr: assert.AnError, messagesErr: assert.AnError,
}, },
@ -355,7 +341,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections_incremental() {
table := []struct { table := []struct {
name string name string
mock mockChannelsBH mock mockBackupHandler
deltaPaths metadata.DeltaPaths deltaPaths metadata.DeltaPaths
expectErr require.ErrorAssertionFunc expectErr require.ErrorAssertionFunc
expectColls int expectColls int
@ -365,7 +351,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections_incremental() {
}{ }{
{ {
name: "non incremental", name: "non incremental",
mock: mockChannelsBH{ mock: mockBackupHandler{
channels: testdata.StubChannels("chan"), channels: testdata.StubChannels("chan"),
messageIDs: []string{"msg"}, messageIDs: []string{"msg"},
}, },
@ -378,7 +364,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections_incremental() {
}, },
{ {
name: "incremental", name: "incremental",
mock: mockChannelsBH{ mock: mockBackupHandler{
channels: testdata.StubChannels("chan"), channels: testdata.StubChannels("chan"),
deletedMsgIDs: []string{"msg"}, deletedMsgIDs: []string{"msg"},
}, },
@ -396,7 +382,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections_incremental() {
}, },
{ {
name: "incremental no new messages", name: "incremental no new messages",
mock: mockChannelsBH{ mock: mockBackupHandler{
channels: testdata.StubChannels("chan"), channels: testdata.StubChannels("chan"),
}, },
deltaPaths: metadata.DeltaPaths{ deltaPaths: metadata.DeltaPaths{
@ -413,7 +399,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections_incremental() {
}, },
{ {
name: "incremental deleted channel", name: "incremental deleted channel",
mock: mockChannelsBH{ mock: mockBackupHandler{
channels: testdata.StubChannels(), channels: testdata.StubChannels(),
}, },
deltaPaths: metadata.DeltaPaths{ deltaPaths: metadata.DeltaPaths{
@ -430,7 +416,7 @@ func (suite *BackupUnitSuite) TestPopulateCollections_incremental() {
}, },
{ {
name: "incremental new and deleted channel", name: "incremental new and deleted channel",
mock: mockChannelsBH{ mock: mockBackupHandler{
channels: testdata.StubChannels("chan2"), channels: testdata.StubChannels("chan2"),
messageIDs: []string{"msg"}, messageIDs: []string{"msg"},
}, },
@ -500,472 +486,15 @@ func (suite *BackupUnitSuite) TestPopulateCollections_incremental() {
} }
} }
// ---------------------------------------------------------------------------
// Conversations tests
// ---------------------------------------------------------------------------
var _ backupHandler[models.Conversationable, models.Postable] = &mockConversationsBH{}
//lint:ignore U1000 false linter issue due to generics
type mockConversationsBH struct {
conversations []models.Conversationable
// Assume all conversations have the same thread object under them for simplicty.
// It doesn't impact the tests.
thread models.ConversationThreadable
postIDs []string
deletedPostIDs []string
PostsErr error
Posts map[string]models.Postable
info map[string]*details.GroupsInfo
getPostErr map[string]error
doNotInclude bool
}
//lint:ignore U1000 false linter issue due to generics
func (bh mockConversationsBH) augmentItemInfo(
*details.GroupsInfo,
models.Conversationable,
) {
// no-op
}
func (bh mockConversationsBH) canMakeDeltaQueries() bool {
return false
}
func (bh mockConversationsBH) containers() []container[models.Conversationable] {
containers := make([]container[models.Conversationable], 0, len(bh.conversations))
for _, ch := range bh.conversations {
containers = append(containers, conversationThreadContainer(ch, bh.thread))
}
return containers
}
//lint:ignore U1000 required for interface compliance
func (bh mockConversationsBH) getContainers(
context.Context,
api.CallConfig,
) ([]container[models.Conversationable], error) {
return bh.containers(), nil
}
func (bh mockConversationsBH) getContainerItemIDs(
_ context.Context,
_ path.Elements,
_ string,
_ api.CallConfig,
) (pagers.AddedAndRemoved, error) {
idRes := make(map[string]time.Time, len(bh.postIDs))
for _, id := range bh.postIDs {
idRes[id] = time.Time{}
}
aar := pagers.AddedAndRemoved{
Added: idRes,
Removed: bh.deletedPostIDs,
ValidModTimes: true,
DU: pagers.DeltaUpdate{},
}
return aar, bh.PostsErr
}
//lint:ignore U1000 required for interface compliance
func (bh mockConversationsBH) includeContainer(
models.Conversationable,
selectors.GroupsScope,
) bool {
return !bh.doNotInclude
}
func (bh mockConversationsBH) canonicalPath(
storageDirFolders path.Elements,
tenantID string,
) (path.Path, error) {
return storageDirFolders.
Builder().
ToDataLayerPath(
tenantID,
"protectedResource",
path.GroupsService,
path.ConversationPostsCategory,
false)
}
//lint:ignore U1000 false linter issue due to generics
func (bh mockConversationsBH) getItem(
_ context.Context,
_ string,
_ path.Elements,
itemID string,
) (models.Postable, *details.GroupsInfo, error) {
return bh.Posts[itemID], bh.info[itemID], bh.getPostErr[itemID]
}
//lint:ignore U1000 false linter issue due to generics
func (bh mockConversationsBH) supportsItemMetadata() bool {
return true
}
func (bh mockConversationsBH) getItemMetadata(
_ context.Context,
_ models.Conversationable,
) (io.ReadCloser, int, error) {
return nil, 0, nil
}
//lint:ignore U1000 false linter issue due to generics
func (bh mockConversationsBH) makeTombstones(
dps metadata.DeltaPaths,
) (map[string]string, error) {
r := make(map[string]string, len(dps))
for id, v := range dps {
elems := path.Split(id)
if len(elems) != 2 {
return nil, clues.New("invalid prev path")
}
r[elems[0]] = v.Path
}
return r, nil
}
func (suite *BackupUnitSuite) TestPopulateCollections_Conversations() {
var (
qp = graph.QueryParams{
Category: path.ConversationPostsCategory, // doesn't matter which one we use.
ProtectedResource: inMock.NewProvider("group_id", "user_name"),
TenantID: suite.creds.AzureTenantID,
}
statusUpdater = func(*support.ControllerOperationStatus) {}
)
table := []struct {
name string
mock mockConversationsBH
expectErr require.ErrorAssertionFunc
expectColls int
expectNewColls int
expectMetadataColls int
}{
{
name: "happy path, one container",
mock: mockConversationsBH{
conversations: testdata.StubConversations("one"),
thread: testdata.StubConversationThreads("t-one")[0],
postIDs: []string{"msg-one"},
},
expectErr: require.NoError,
expectColls: 2,
expectNewColls: 1,
expectMetadataColls: 1,
},
{
name: "happy path, one container, only deleted messages",
mock: mockConversationsBH{
conversations: testdata.StubConversations("one"),
thread: testdata.StubConversationThreads("t-one")[0],
deletedPostIDs: []string{"msg-one"},
},
expectErr: require.NoError,
expectColls: 2,
expectNewColls: 1,
expectMetadataColls: 1,
},
{
name: "happy path, many containers",
mock: mockConversationsBH{
conversations: testdata.StubConversations("one", "two"),
thread: testdata.StubConversationThreads("t-one")[0],
postIDs: []string{"msg-one"},
},
expectErr: require.NoError,
expectColls: 3,
expectNewColls: 2,
expectMetadataColls: 1,
},
{
name: "no containers pass scope",
mock: mockConversationsBH{
conversations: testdata.StubConversations("one"),
thread: testdata.StubConversationThreads("t-one")[0],
doNotInclude: true,
},
expectErr: require.NoError,
expectColls: 1,
expectNewColls: 0,
expectMetadataColls: 1,
},
{
name: "no conversations",
mock: mockConversationsBH{},
expectErr: require.NoError,
expectColls: 1,
expectNewColls: 0,
expectMetadataColls: 1,
},
{
name: "no conv posts",
mock: mockConversationsBH{
conversations: testdata.StubConversations("one"),
thread: testdata.StubConversationThreads("t-one")[0],
},
expectErr: require.NoError,
expectColls: 2,
expectNewColls: 1,
expectMetadataColls: 1,
},
{
name: "err: deleted in flight",
mock: mockConversationsBH{
conversations: testdata.StubConversations("one"),
thread: testdata.StubConversationThreads("t-one")[0],
PostsErr: core.ErrNotFound,
},
expectErr: require.Error,
expectColls: 1,
expectNewColls: 0,
expectMetadataColls: 1,
},
{
name: "err: other error",
mock: mockConversationsBH{
conversations: testdata.StubConversations("one"),
thread: testdata.StubConversationThreads("t-one")[0],
PostsErr: assert.AnError,
},
expectErr: require.Error,
expectColls: 1,
expectNewColls: 0,
expectMetadataColls: 1,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
ctrlOpts := control.Options{FailureHandling: control.FailFast}
collections, err := populateCollections(
ctx,
qp,
test.mock,
statusUpdater,
test.mock.containers(),
selectors.NewGroupsBackup(nil).Channels(selectors.Any())[0],
nil,
false,
ctrlOpts,
count.New(),
fault.New(true))
test.expectErr(t, err, clues.ToCore(err))
assert.Len(t, collections, test.expectColls, "number of collections")
// collection assertions
deleteds, news, metadatas, doNotMerges := 0, 0, 0, 0
for _, c := range collections {
if c.FullPath().Service() == path.GroupsMetadataService {
metadatas++
continue
}
if c.State() == data.DeletedState {
deleteds++
}
if c.State() == data.NewState {
news++
}
if c.DoNotMergeItems() {
doNotMerges++
}
}
assert.Zero(t, deleteds, "deleted collections")
assert.Equal(t, test.expectNewColls, news, "new collections")
assert.Equal(t, test.expectMetadataColls, metadatas, "metadata collections")
})
}
}
func (suite *BackupUnitSuite) TestPopulateCollections_ConversationsIncremental() {
var (
qp = graph.QueryParams{
Category: path.ConversationPostsCategory, // doesn't matter which one we use.
ProtectedResource: inMock.NewProvider("group_id", "user_name"),
TenantID: suite.creds.AzureTenantID,
}
statusUpdater = func(*support.ControllerOperationStatus) {}
allScope = selectors.NewGroupsBackup(nil).Conversation(selectors.Any())[0]
)
convPath, err := path.Build("t", "g", path.GroupsService, path.ConversationPostsCategory, false, "conv0", "thread0")
require.NoError(suite.T(), err, clues.ToCore(err))
table := []struct {
name string
mock mockConversationsBH
deltaPaths metadata.DeltaPaths
expectErr require.ErrorAssertionFunc
expectColls int
expectNewColls int
expectTombstoneCols int
expectMetadataColls int
}{
{
name: "non incremental",
mock: mockConversationsBH{
conversations: testdata.StubConversations("conv0"),
thread: testdata.StubConversationThreads("t0")[0],
postIDs: []string{"msg"},
},
deltaPaths: metadata.DeltaPaths{},
expectErr: require.NoError,
expectColls: 2,
expectNewColls: 1,
expectTombstoneCols: 0,
expectMetadataColls: 1,
},
{
name: "incremental",
mock: mockConversationsBH{
conversations: testdata.StubConversations("conv0"),
thread: testdata.StubConversationThreads("t0")[0],
postIDs: []string{"msg"},
},
deltaPaths: metadata.DeltaPaths{
"conv0/thread0": {
Path: convPath.String(),
},
},
expectErr: require.NoError,
expectColls: 2,
expectNewColls: 1, // No delta support
expectTombstoneCols: 0,
expectMetadataColls: 1,
},
{
name: "incremental no new posts",
mock: mockConversationsBH{
conversations: testdata.StubConversations("conv0"),
thread: testdata.StubConversationThreads("t0")[0],
},
deltaPaths: metadata.DeltaPaths{
"conv0/thread0": {
Path: convPath.String(),
},
},
expectErr: require.NoError,
expectColls: 2,
expectNewColls: 1, // No delta support
expectTombstoneCols: 0,
expectMetadataColls: 1,
},
{
name: "incremental deleted conversation",
mock: mockConversationsBH{
conversations: testdata.StubConversations(),
},
deltaPaths: metadata.DeltaPaths{
"conv0/thread0": {
Path: convPath.String(),
},
},
expectErr: require.NoError,
expectColls: 2,
expectNewColls: 0,
expectTombstoneCols: 1,
expectMetadataColls: 1,
},
{
name: "incremental new and deleted conversations",
mock: mockConversationsBH{
conversations: testdata.StubConversations("conv1"),
thread: testdata.StubConversationThreads("t1")[0],
postIDs: []string{"msg"},
},
deltaPaths: metadata.DeltaPaths{
"conv0/thread0": {
Path: convPath.String(),
},
},
expectErr: require.NoError,
expectColls: 3,
expectNewColls: 1,
expectTombstoneCols: 1,
expectMetadataColls: 1,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
ctrlOpts := control.Options{FailureHandling: control.FailFast}
collections, err := populateCollections(
ctx,
qp,
test.mock,
statusUpdater,
test.mock.containers(),
allScope,
test.deltaPaths,
false,
ctrlOpts,
count.New(),
fault.New(true))
test.expectErr(t, err, clues.ToCore(err))
assert.Len(t, collections, test.expectColls, "number of collections")
// collection assertions
tombstones, news, metadatas, doNotMerges := 0, 0, 0, 0
for _, c := range collections {
if c.FullPath() != nil && c.FullPath().Service() == path.GroupsMetadataService {
metadatas++
continue
}
if c.State() == data.DeletedState {
tombstones++
}
if c.State() == data.NewState {
news++
}
if c.DoNotMergeItems() {
doNotMerges++
}
}
assert.Equal(t, test.expectNewColls, news, "new collections")
assert.Equal(t, test.expectTombstoneCols, tombstones, "tombstone collections")
assert.Equal(t, test.expectMetadataColls, metadatas, "metadata collections")
})
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Integration tests // Integration tests
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
type BackupIntgSuite struct { type BackupIntgSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup resource string
tenantID string
ac api.Client
} }
func TestBackupIntgSuite(t *testing.T) { func TestBackupIntgSuite(t *testing.T) {
@ -978,19 +507,32 @@ func TestBackupIntgSuite(t *testing.T) {
func (suite *BackupIntgSuite) SetupSuite() { func (suite *BackupIntgSuite) SetupSuite() {
t := suite.T() t := suite.T()
suite.m365 = its.GetM365(t)
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
graph.InitializeConcurrencyLimiter(ctx, true, 4) graph.InitializeConcurrencyLimiter(ctx, true, 4)
suite.resource = tconfig.M365TeamID(t)
acct := tconfig.NewM365Account(t)
creds, err := acct.M365Config()
require.NoError(t, err, clues.ToCore(err))
suite.ac, err = api.NewClient(
creds,
control.DefaultOptions(),
count.New())
require.NoError(t, err, clues.ToCore(err))
suite.tenantID = creds.AzureTenantID
} }
func (suite *BackupIntgSuite) TestCreateCollections() { func (suite *BackupIntgSuite) TestCreateCollections() {
var ( var (
protectedResource = suite.m365.Group.ID protectedResource = tconfig.M365TeamID(suite.T())
resources = []string{suite.m365.Group.ID} resources = []string{protectedResource}
handler = NewChannelBackupHandler(protectedResource, suite.m365.AC.Channels()) handler = NewChannelBackupHandler(protectedResource, suite.ac.Channels())
) )
tests := []struct { tests := []struct {
@ -1016,13 +558,13 @@ func (suite *BackupIntgSuite) TestCreateCollections() {
ctrlOpts := control.DefaultOptions() ctrlOpts := control.DefaultOptions()
sel := selectors.NewGroupsBackup([]string{suite.m365.Group.ID}) sel := selectors.NewGroupsBackup([]string{protectedResource})
sel.Include(selTD.GroupsBackupChannelScope(sel)) sel.Include(selTD.GroupsBackupChannelScope(sel))
bpc := inject.BackupProducerConfig{ bpc := inject.BackupProducerConfig{
LastBackupVersion: version.NoBackup, LastBackupVersion: version.NoBackup,
Options: ctrlOpts, Options: ctrlOpts,
ProtectedResource: suite.m365.Group.Provider, ProtectedResource: inMock.NewProvider(protectedResource, protectedResource),
Selector: sel.Selector, Selector: sel.Selector,
} }
@ -1030,7 +572,7 @@ func (suite *BackupIntgSuite) TestCreateCollections() {
ctx, ctx,
bpc, bpc,
handler, handler,
suite.m365.TenantID, suite.tenantID,
test.scope, test.scope,
func(status *support.ControllerOperationStatus) {}, func(status *support.ControllerOperationStatus) {},
false, false,

View File

@ -9,7 +9,6 @@ import (
"github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/backup/metadata"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/selectors"
"github.com/alcionai/corso/src/pkg/services/m365/api" "github.com/alcionai/corso/src/pkg/services/m365/api"
@ -132,12 +131,6 @@ func (bh channelsBackupHandler) supportsItemMetadata() bool {
return false return false
} }
func (bh channelsBackupHandler) makeTombstones(
dps metadata.DeltaPaths,
) (map[string]string, error) {
return makeTombstones(dps), nil
}
func channelContainer(ch models.Channelable) container[models.Channelable] { func channelContainer(ch models.Channelable) container[models.Channelable] {
return container[models.Channelable]{ return container[models.Channelable]{
storageDirFolders: path.Elements{ptr.Val(ch.GetId())}, storageDirFolders: path.Elements{ptr.Val(ch.GetId())},

View File

@ -12,7 +12,6 @@ import (
"github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/m365/collection/groups/metadata" "github.com/alcionai/corso/src/internal/m365/collection/groups/metadata"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
deltaPath "github.com/alcionai/corso/src/pkg/backup/metadata"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/selectors"
"github.com/alcionai/corso/src/pkg/services/m365/api" "github.com/alcionai/corso/src/pkg/services/m365/api"
@ -176,36 +175,6 @@ func (bh conversationsBackupHandler) supportsItemMetadata() bool {
return true return true
} }
func (bh conversationsBackupHandler) makeTombstones(
dps deltaPath.DeltaPaths,
) (map[string]string, error) {
r := make(map[string]string, len(dps))
for id, v := range dps {
// ID is of format conversationID/threadID. Tombstones are looked up
// by conversationID only, so remove the threadID part. This is safe
// because every conversation has only one thread.
elems := path.Split(id)
if len(elems) != 2 {
return nil, clues.New("invalid prev path")
}
r[elems[0]] = v.Path
}
// We are assuming a 1:1 mapping between conversations and threads. While
// this is true today, graph behavior may change in future. Throw an error
// if the assumption is violated.
//
// We cannot catch this error with tests because creating conversations
// requires delegated access.
if len(dps) != len(r) {
return nil, clues.New("multiple threads exist for a conversation")
}
return r, nil
}
func conversationThreadContainer( func conversationThreadContainer(
c models.Conversationable, c models.Conversationable,
t models.ConversationThreadable, t models.ConversationThreadable,

View File

@ -14,7 +14,6 @@ import (
"github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/m365/collection/groups/metadata" "github.com/alcionai/corso/src/internal/m365/collection/groups/metadata"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
deltaPath "github.com/alcionai/corso/src/pkg/backup/metadata"
) )
const ( const (
@ -65,72 +64,3 @@ func (suite *ConversationHandlerUnitSuite) TestGetItemMetadata() {
assert.Equal(t, []string{resourceEmail}, meta.Recipients, "incorrect recipients") assert.Equal(t, []string{resourceEmail}, meta.Recipients, "incorrect recipients")
assert.Equal(t, ptr.Val(conv.GetTopic()), meta.Topic, "incorrect topic") assert.Equal(t, ptr.Val(conv.GetTopic()), meta.Topic, "incorrect topic")
} }
func (suite *ConversationHandlerUnitSuite) TestMakeTombstones() {
table := []struct {
name string
dps deltaPath.DeltaPaths
expected map[string]string
expectedErr require.ErrorAssertionFunc
}{
{
name: "valid",
dps: deltaPath.DeltaPaths{
"c1/t1": deltaPath.DeltaPath{
Path: "p1",
},
"c2/t2": deltaPath.DeltaPath{
Path: "p2",
},
},
expected: map[string]string{
"c1": "p1",
"c2": "p2",
},
expectedErr: require.NoError,
},
{
name: "invalid prev path",
dps: deltaPath.DeltaPaths{
"c1": deltaPath.DeltaPath{
Path: "p1",
},
},
expected: nil,
expectedErr: require.Error,
},
{
name: "invalid prev path 2",
dps: deltaPath.DeltaPaths{
"c1/t1/a1": deltaPath.DeltaPath{
Path: "p1",
},
},
expected: nil,
expectedErr: require.Error,
},
{
name: "multiple threads exist for a conversation",
dps: deltaPath.DeltaPaths{
"c1/t1": deltaPath.DeltaPath{
Path: "p1",
},
"c1/t2": deltaPath.DeltaPath{
Path: "p2",
},
},
expected: nil,
expectedErr: require.Error,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
bh := conversationsBackupHandler{}
result, err := bh.makeTombstones(test.dps)
test.expectedErr(t, err)
assert.Equal(t, test.expected, result)
})
}
}

View File

@ -7,7 +7,6 @@ import (
"github.com/microsoft/kiota-abstractions-go/serialization" "github.com/microsoft/kiota-abstractions-go/serialization"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/backup/metadata"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/selectors"
"github.com/alcionai/corso/src/pkg/services/m365/api" "github.com/alcionai/corso/src/pkg/services/m365/api"
@ -31,7 +30,6 @@ type backupHandler[C graph.GetIDer, I groupsItemer] interface {
includeContainerer[C] includeContainerer[C]
canonicalPather canonicalPather
canMakeDeltaQuerieser canMakeDeltaQuerieser
makeTombstoneser
} }
type getItemAndAugmentInfoer[C graph.GetIDer, I groupsItemer] interface { type getItemAndAugmentInfoer[C graph.GetIDer, I groupsItemer] interface {
@ -109,13 +107,6 @@ type canMakeDeltaQuerieser interface {
canMakeDeltaQueries() bool canMakeDeltaQueries() bool
} }
// produces a set of id:path pairs from the deltapaths map.
// Each entry in the set will, if not removed, produce a collection
// that will delete the tombstone by path.
type makeTombstoneser interface {
makeTombstones(dps metadata.DeltaPaths) (map[string]string, error)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Container management // Container management
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -22,14 +22,12 @@ func parseMetadataCollections(
// cdp stores metadata // cdp stores metadata
cdp := metadata.CatDeltaPaths{ cdp := metadata.CatDeltaPaths{
path.ChannelMessagesCategory: {}, path.ChannelMessagesCategory: {},
path.ConversationPostsCategory: {},
} }
// found tracks the metadata we've loaded, to make sure we don't // found tracks the metadata we've loaded, to make sure we don't
// fetch overlapping copies. // fetch overlapping copies.
found := map[path.CategoryType]map[string]struct{}{ found := map[path.CategoryType]map[string]struct{}{
path.ChannelMessagesCategory: {}, path.ChannelMessagesCategory: {},
path.ConversationPostsCategory: {},
} }
// errors from metadata items should not stop the backup, // errors from metadata items should not stop the backup,
@ -107,7 +105,6 @@ func parseMetadataCollections(
return metadata.CatDeltaPaths{ return metadata.CatDeltaPaths{
path.ChannelMessagesCategory: {}, path.ChannelMessagesCategory: {},
path.ConversationPostsCategory: {},
}, false, nil }, false, nil
} }

View File

@ -1,52 +0,0 @@
package testdata
import (
"github.com/google/uuid"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/internal/common/ptr"
)
func StubConversations(ids ...string) []models.Conversationable {
sl := make([]models.Conversationable, 0, len(ids))
for _, id := range ids {
c := models.NewConversation()
c.SetId(ptr.To(id))
sl = append(sl, c)
}
return sl
}
func StubConversationThreads(ids ...string) []models.ConversationThreadable {
sl := make([]models.ConversationThreadable, 0, len(ids))
for _, id := range ids {
ct := models.NewConversationThread()
ct.SetId(ptr.To(id))
sl = append(sl, ct)
}
return sl
}
func StubPosts(ids ...string) []models.Postable {
sl := make([]models.Postable, 0, len(ids))
for _, id := range ids {
p := models.NewPost()
p.SetId(ptr.To(uuid.NewString()))
body := models.NewItemBody()
body.SetContent(ptr.To(id))
p.SetBody(body)
sl = append(sl, p)
}
return sl
}

View File

@ -69,7 +69,7 @@ func CollectLibraries(
odcs, canUsePreviousBackup, err := colls.Get(ctx, bpc.MetadataCollections, ssmb, errs) odcs, canUsePreviousBackup, err := colls.Get(ctx, bpc.MetadataCollections, ssmb, errs)
if err != nil { if err != nil {
return nil, false, clues.Wrap(err, "getting library") return nil, false, graph.Wrap(ctx, err, "getting library")
} }
return append(collections, odcs...), canUsePreviousBackup, nil return append(collections, odcs...), canUsePreviousBackup, nil

View File

@ -18,7 +18,6 @@ import (
"github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/m365/support"
"github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/internal/operations/inject"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/internal/version" "github.com/alcionai/corso/src/internal/version"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
@ -329,49 +328,59 @@ func (suite *SharePointBackupUnitSuite) TestPopulateListsCollections_incremental
} }
} }
type SharePointBackupIntgSuite struct { type SharePointSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup
} }
func TestSharePointSuite(t *testing.T) { func TestSharePointSuite(t *testing.T) {
suite.Run(t, &SharePointBackupIntgSuite{ suite.Run(t, &SharePointSuite{
Suite: tester.NewIntegrationSuite( Suite: tester.NewIntegrationSuite(
t, t,
[][]string{tconfig.M365AcctCredEnvs}), [][]string{tconfig.M365AcctCredEnvs}),
}) })
} }
func (suite *SharePointBackupIntgSuite) SetupSuite() { func (suite *SharePointSuite) SetupSuite() {
t := suite.T() ctx, flush := tester.NewContext(suite.T())
suite.m365 = its.GetM365(t)
ctx, flush := tester.NewContext(t)
defer flush() defer flush()
graph.InitializeConcurrencyLimiter(ctx, false, 4) graph.InitializeConcurrencyLimiter(ctx, false, 4)
} }
func (suite *SharePointBackupIntgSuite) TestCollectPages() { func (suite *SharePointSuite) TestCollectPages() {
t := suite.T() t := suite.T()
counter := count.New()
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
var (
siteID = tconfig.M365SiteID(t)
a = tconfig.NewM365Account(t)
counter = count.New()
)
creds, err := a.M365Config()
require.NoError(t, err, clues.ToCore(err))
ac, err := api.NewClient(
creds,
control.DefaultOptions(),
counter)
require.NoError(t, err, clues.ToCore(err))
bpc := inject.BackupProducerConfig{ bpc := inject.BackupProducerConfig{
LastBackupVersion: version.NoBackup, LastBackupVersion: version.NoBackup,
Options: control.DefaultOptions(), Options: control.DefaultOptions(),
ProtectedResource: suite.m365.Site.Provider, ProtectedResource: mock.NewProvider(siteID, siteID),
} }
sel := selectors.NewSharePointBackup([]string{suite.m365.Site.ID}) sel := selectors.NewSharePointBackup([]string{siteID})
col, err := CollectPages( col, err := CollectPages(
ctx, ctx,
bpc, bpc,
suite.m365.Creds, creds,
suite.m365.AC, ac,
sel.Lists(selectors.Any())[0], sel.Lists(selectors.Any())[0],
(&MockGraphService{}).UpdateStatus, (&MockGraphService{}).UpdateStatus,
counter, counter,
@ -380,27 +389,43 @@ func (suite *SharePointBackupIntgSuite) TestCollectPages() {
assert.NotEmpty(t, col) assert.NotEmpty(t, col)
} }
func (suite *SharePointBackupIntgSuite) TestCollectLists() { func (suite *SharePointSuite) TestCollectLists() {
t := suite.T() t := suite.T()
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
var (
siteID = tconfig.M365SiteID(t)
a = tconfig.NewM365Account(t)
counter = count.New()
)
creds, err := a.M365Config()
require.NoError(t, err, clues.ToCore(err))
ac, err := api.NewClient(
creds,
control.DefaultOptions(),
counter)
require.NoError(t, err, clues.ToCore(err))
bpc := inject.BackupProducerConfig{ bpc := inject.BackupProducerConfig{
LastBackupVersion: version.NoBackup, LastBackupVersion: version.NoBackup,
Options: control.DefaultOptions(), Options: control.DefaultOptions(),
ProtectedResource: suite.m365.Site.Provider, ProtectedResource: mock.NewProvider(siteID, siteID),
} }
sel := selectors.NewSharePointBackup([]string{suite.m365.Site.ID}) sel := selectors.NewSharePointBackup([]string{siteID})
bh := NewListsBackupHandler(suite.m365.Site.ID, suite.m365.AC.Lists())
bh := NewListsBackupHandler(bpc.ProtectedResource.ID(), ac.Lists())
col, _, err := CollectLists( col, _, err := CollectLists(
ctx, ctx,
bh, bh,
bpc, bpc,
suite.m365.AC, ac,
suite.m365.Creds.AzureTenantID, creds.AzureTenantID,
sel.Lists(selectors.Any())[0], sel.Lists(selectors.Any())[0],
(&MockGraphService{}).UpdateStatus, (&MockGraphService{}).UpdateStatus,
count.New(), count.New(),
@ -420,7 +445,7 @@ func (suite *SharePointBackupIntgSuite) TestCollectLists() {
assert.True(t, metadataFound) assert.True(t, metadataFound)
} }
func (suite *SharePointBackupIntgSuite) TestParseListsMetadataCollections() { func (suite *SharePointSuite) TestParseListsMetadataCollections() {
type fileValues struct { type fileValues struct {
fileName string fileName string
value string value string
@ -555,7 +580,7 @@ func (f failingColl) FetchItemByName(context.Context, string) (data.Item, error)
return nil, nil return nil, nil
} }
func (suite *SharePointBackupIntgSuite) TestParseListsMetadataCollections_ReadFailure() { func (suite *SharePointSuite) TestParseListsMetadataCollections_ReadFailure() {
t := suite.T() t := suite.T()
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)

View File

@ -541,12 +541,12 @@ func serializeContent(
err := writer.WriteObjectValue("", obj) err := writer.WriteObjectValue("", obj)
if err != nil { if err != nil {
return nil, clues.WrapWC(ctx, err, "writing to serializer").Label(fault.LabelForceNoBackupCreation) return nil, graph.Wrap(ctx, err, "writing to serializer").Label(fault.LabelForceNoBackupCreation)
} }
byteArray, err := writer.GetSerializedContent() byteArray, err := writer.GetSerializedContent()
if err != nil { if err != nil {
return nil, clues.WrapWC(ctx, err, "getting content from writer").Label(fault.LabelForceNoBackupCreation) return nil, graph.Wrap(ctx, err, "getting content from writer").Label(fault.LabelForceNoBackupCreation)
} }
return byteArray, nil return byteArray, nil

View File

@ -20,7 +20,6 @@ import (
spMock "github.com/alcionai/corso/src/internal/m365/service/sharepoint/mock" spMock "github.com/alcionai/corso/src/internal/m365/service/sharepoint/mock"
"github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/m365/support"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
@ -114,17 +113,34 @@ func (suite *SharePointCollectionUnitSuite) TestPrefetchCollection_state() {
} }
} }
type SharePointCollIntgSuite struct { type SharePointCollectionSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup siteID string
creds account.M365Config
ac api.Client
} }
func (suite *SharePointCollIntgSuite) SetupSuite() { func (suite *SharePointCollectionSuite) SetupSuite() {
suite.m365 = its.GetM365(suite.T()) t := suite.T()
suite.siteID = tconfig.M365SiteID(t)
a := tconfig.NewM365Account(t)
m365, err := a.M365Config()
require.NoError(t, err, clues.ToCore(err))
suite.creds = m365
ac, err := api.NewClient(
m365,
control.DefaultOptions(),
count.New())
require.NoError(t, err, clues.ToCore(err))
suite.ac = ac
} }
func TestSharePointCollectionSuite(t *testing.T) { func TestSharePointCollectionSuite(t *testing.T) {
suite.Run(t, &SharePointCollIntgSuite{ suite.Run(t, &SharePointCollectionSuite{
Suite: tester.NewIntegrationSuite( Suite: tester.NewIntegrationSuite(
t, t,
[][]string{tconfig.M365AcctCredEnvs}), [][]string{tconfig.M365AcctCredEnvs}),
@ -133,13 +149,15 @@ func TestSharePointCollectionSuite(t *testing.T) {
// TestListCollection tests basic functionality to create // TestListCollection tests basic functionality to create
// SharePoint collection and to use the data stream channel. // SharePoint collection and to use the data stream channel.
func (suite *SharePointCollIntgSuite) TestPrefetchCollection_Items() { func (suite *SharePointCollectionSuite) TestPrefetchCollection_Items() {
var ( var (
tenant = "some"
user = "user"
prevRoot = "prev" prevRoot = "prev"
dirRoot = "directory" dirRoot = "directory"
) )
sel := selectors.NewSharePointBackup([]string{suite.m365.Site.ID}) sel := selectors.NewSharePointBackup([]string{"site"})
tables := []struct { tables := []struct {
name, itemName string name, itemName string
@ -165,8 +183,8 @@ func (suite *SharePointCollIntgSuite) TestPrefetchCollection_Items() {
getter: &mock.ListHandler{}, getter: &mock.ListHandler{},
getDir: func(t *testing.T, root string) path.Path { getDir: func(t *testing.T, root string) path.Path {
dir, err := path.Build( dir, err := path.Build(
suite.m365.TenantID, tenant,
suite.m365.User.ID, user,
path.SharePointService, path.SharePointService,
path.ListsCategory, path.ListsCategory,
false, false,
@ -214,8 +232,8 @@ func (suite *SharePointCollIntgSuite) TestPrefetchCollection_Items() {
getter: nil, getter: nil,
getDir: func(t *testing.T, root string) path.Path { getDir: func(t *testing.T, root string) path.Path {
dir, err := path.Build( dir, err := path.Build(
suite.m365.TenantID, tenant,
suite.m365.User.ID, user,
path.SharePointService, path.SharePointService,
path.PagesCategory, path.PagesCategory,
false, false,
@ -252,7 +270,7 @@ func (suite *SharePointCollIntgSuite) TestPrefetchCollection_Items() {
test.getDir(t, test.curr), test.getDir(t, test.curr),
test.getDir(t, test.prev), test.getDir(t, test.prev),
test.locPb, test.locPb,
suite.m365.AC, suite.ac,
test.scope, test.scope,
nil, nil,
control.DefaultOptions(), control.DefaultOptions(),
@ -288,7 +306,7 @@ func (suite *SharePointCollIntgSuite) TestPrefetchCollection_Items() {
} }
} }
func (suite *SharePointCollIntgSuite) TestLazyCollection_Items() { func (suite *SharePointCollectionSuite) TestLazyCollection_Items() {
var ( var (
t = suite.T() t = suite.T()
errs = fault.New(true) errs = fault.New(true)
@ -398,7 +416,7 @@ func (suite *SharePointCollIntgSuite) TestLazyCollection_Items() {
} }
} }
func (suite *SharePointCollIntgSuite) TestLazyItem() { func (suite *SharePointCollectionSuite) TestLazyItem() {
var ( var (
t = suite.T() t = suite.T()
now = time.Now() now = time.Now()
@ -442,7 +460,7 @@ func (suite *SharePointCollIntgSuite) TestLazyItem() {
assert.Equal(t, now, info.Modified()) assert.Equal(t, now, info.Modified())
} }
func (suite *SharePointCollIntgSuite) TestLazyItem_ReturnsEmptyReaderOnDeletedInFlight() { func (suite *SharePointCollectionSuite) TestLazyItem_ReturnsEmptyReaderOnDeletedInFlight() {
var ( var (
t = suite.T() t = suite.T()
now = time.Now() now = time.Now()

View File

@ -103,6 +103,7 @@ func restoreListItem(
} }
// Restore to List base to M365 back store // Restore to List base to M365 back store
dii.SharePoint = api.ListToSPInfo(restoredList) dii.SharePoint = api.ListToSPInfo(restoredList)
return dii, nil return dii, nil

View File

@ -21,8 +21,8 @@ import (
siteMock "github.com/alcionai/corso/src/internal/m365/collection/site/mock" siteMock "github.com/alcionai/corso/src/internal/m365/collection/site/mock"
spMock "github.com/alcionai/corso/src/internal/m365/service/sharepoint/mock" spMock "github.com/alcionai/corso/src/internal/m365/service/sharepoint/mock"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/control/testdata" "github.com/alcionai/corso/src/pkg/control/testdata"
@ -87,17 +87,32 @@ func (suite *SharePointCollectionUnitSuite) TestFormatListsRestoreDestination()
type SharePointRestoreSuite struct { type SharePointRestoreSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup siteID string
creds account.M365Config
ac api.Client
} }
func (suite *SharePointRestoreSuite) SetupSuite() { func (suite *SharePointRestoreSuite) SetupSuite() {
t := suite.T() t := suite.T()
suite.m365 = its.GetM365(t)
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
graph.InitializeConcurrencyLimiter(ctx, false, 4) graph.InitializeConcurrencyLimiter(ctx, false, 4)
suite.siteID = tconfig.M365SiteID(t)
a := tconfig.NewM365Account(t)
m365, err := a.M365Config()
require.NoError(t, err, clues.ToCore(err))
suite.creds = m365
ac, err := api.NewClient(
m365,
control.DefaultOptions(),
count.New())
require.NoError(t, err, clues.ToCore(err))
suite.ac = ac
} }
func TestSharePointRestoreSuite(t *testing.T) { func TestSharePointRestoreSuite(t *testing.T) {
@ -120,8 +135,8 @@ func (suite *SharePointRestoreSuite) TestListCollection_Restore() {
listTemplate = "genericList" listTemplate = "genericList"
restoreCfg = testdata.DefaultRestoreConfig("") restoreCfg = testdata.DefaultRestoreConfig("")
destName = restoreCfg.Location destName = restoreCfg.Location
lrh = NewListsRestoreHandler(suite.m365.Site.ID, suite.m365.AC.Lists()) lrh = NewListsRestoreHandler(suite.siteID, suite.ac.Lists())
service = createTestService(t, suite.m365.Creds) service = createTestService(t, suite.creds)
list = stubList(listTemplate, listName) list = stubList(listTemplate, listName)
mockData = generateListData(t, service, list) mockData = generateListData(t, service, list)
) )
@ -132,7 +147,7 @@ func (suite *SharePointRestoreSuite) TestListCollection_Restore() {
ctx, ctx,
lrh, lrh,
mockData, mockData,
suite.m365.Site.ID, suite.siteID,
restoreCfg, restoreCfg,
nil, nil,
count.New(), count.New(),
@ -141,7 +156,7 @@ func (suite *SharePointRestoreSuite) TestListCollection_Restore() {
assert.Equal(t, fmt.Sprintf("%s_%s", destName, listName), deets.SharePoint.List.Name) assert.Equal(t, fmt.Sprintf("%s_%s", destName, listName), deets.SharePoint.List.Name)
// Clean-Up // Clean-Up
deleteList(ctx, t, suite.m365.Site.ID, lrh, deets) deleteList(ctx, t, suite.siteID, lrh, deets)
} }
func (suite *SharePointRestoreSuite) TestListCollection_Restore_invalidListTemplate() { func (suite *SharePointRestoreSuite) TestListCollection_Restore_invalidListTemplate() {
@ -151,10 +166,10 @@ func (suite *SharePointRestoreSuite) TestListCollection_Restore_invalidListTempl
defer flush() defer flush()
var ( var (
lrh = NewListsRestoreHandler(suite.m365.Site.ID, suite.m365.AC.Lists()) lrh = NewListsRestoreHandler(suite.siteID, suite.ac.Lists())
listName = "MockListing" listName = "MockListing"
restoreCfg = testdata.DefaultRestoreConfig("") restoreCfg = testdata.DefaultRestoreConfig("")
service = createTestService(t, suite.m365.Creds) service = createTestService(t, suite.creds)
) )
restoreCfg.OnCollision = control.Copy restoreCfg.OnCollision = control.Copy
@ -186,7 +201,7 @@ func (suite *SharePointRestoreSuite) TestListCollection_Restore_invalidListTempl
ctx, ctx,
lrh, lrh,
listData, listData,
suite.m365.Site.ID, suite.siteID,
restoreCfg, restoreCfg,
nil, nil,
count.New(), count.New(),
@ -207,8 +222,8 @@ func (suite *SharePointRestoreSuite) TestListCollection_RestoreInPlace_skip() {
listName = "MockListing" listName = "MockListing"
listTemplate = "genericList" listTemplate = "genericList"
restoreCfg = testdata.DefaultRestoreConfig("") restoreCfg = testdata.DefaultRestoreConfig("")
lrh = NewListsRestoreHandler(suite.m365.Site.ID, suite.m365.AC.Lists()) lrh = NewListsRestoreHandler(suite.siteID, suite.ac.Lists())
service = createTestService(t, suite.m365.Creds) service = createTestService(t, suite.creds)
list = stubList(listTemplate, listName) list = stubList(listTemplate, listName)
newList = stubList(listTemplate, listName) newList = stubList(listTemplate, listName)
cl = count.New() cl = count.New()
@ -224,7 +239,7 @@ func (suite *SharePointRestoreSuite) TestListCollection_RestoreInPlace_skip() {
ctx, ctx,
lrh, lrh,
mockData, mockData,
suite.m365.Site.ID, suite.siteID,
restoreCfg, // OnCollision is skip by default restoreCfg, // OnCollision is skip by default
collisionKeyToItemID, collisionKeyToItemID,
cl, cl,
@ -246,7 +261,7 @@ func (suite *SharePointRestoreSuite) TestListCollection_RestoreInPlace_copy() {
listTemplate = "genericList" listTemplate = "genericList"
listID = "some-list-id" listID = "some-list-id"
restoreCfg = testdata.DefaultRestoreConfig("") restoreCfg = testdata.DefaultRestoreConfig("")
service = createTestService(t, suite.m365.Creds) service = createTestService(t, suite.creds)
policyToKey = map[control.CollisionPolicy]count.Key{ policyToKey = map[control.CollisionPolicy]count.Key{
control.Replace: count.CollisionReplace, control.Replace: count.CollisionReplace,
@ -339,7 +354,7 @@ func (suite *SharePointRestoreSuite) TestListCollection_RestoreInPlace_copy() {
ctx, ctx,
test.lrh, test.lrh,
mockData, mockData,
suite.m365.Site.ID, suite.siteID,
restoreCfg, restoreCfg,
collisionKeyToItemID, collisionKeyToItemID,
cl, cl,

View File

@ -17,7 +17,6 @@ import (
"github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/m365/support"
"github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/internal/operations/inject"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/internal/version" "github.com/alcionai/corso/src/internal/version"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
@ -256,7 +255,9 @@ func (suite *BackupUnitSuite) TestPopulateCollections() {
type BackupIntgSuite struct { type BackupIntgSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup resource string
tenantID string
ac api.Client
} }
func TestBackupIntgSuite(t *testing.T) { func TestBackupIntgSuite(t *testing.T) {
@ -269,20 +270,33 @@ func TestBackupIntgSuite(t *testing.T) {
func (suite *BackupIntgSuite) SetupSuite() { func (suite *BackupIntgSuite) SetupSuite() {
t := suite.T() t := suite.T()
suite.m365 = its.GetM365(t)
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
graph.InitializeConcurrencyLimiter(ctx, true, 4) graph.InitializeConcurrencyLimiter(ctx, true, 4)
suite.resource = tconfig.M365TeamID(t)
acct := tconfig.NewM365Account(t)
creds, err := acct.M365Config()
require.NoError(t, err, clues.ToCore(err))
suite.ac, err = api.NewClient(
creds,
control.DefaultOptions(),
count.New())
require.NoError(t, err, clues.ToCore(err))
suite.tenantID = creds.AzureTenantID
} }
func (suite *BackupIntgSuite) TestCreateCollections() { func (suite *BackupIntgSuite) TestCreateCollections() {
var ( var (
tenant = suite.m365.TenantID tenant = tconfig.M365TenantID(suite.T())
protectedResource = suite.m365.Group.ID protectedResource = tconfig.M365TeamID(suite.T())
resources = []string{protectedResource} resources = []string{protectedResource}
handler = NewUsersChatsBackupHandler(tenant, protectedResource, suite.m365.AC.Chats()) handler = NewUsersChatsBackupHandler(tenant, protectedResource, suite.ac.Chats())
) )
tests := []struct { tests := []struct {
@ -308,13 +322,13 @@ func (suite *BackupIntgSuite) TestCreateCollections() {
ctrlOpts := control.DefaultOptions() ctrlOpts := control.DefaultOptions()
sel := selectors.NewTeamsChatsBackup([]string{suite.m365.Group.ID}) sel := selectors.NewTeamsChatsBackup([]string{protectedResource})
sel.Include(selTD.TeamsChatsBackupChatScope(sel)) sel.Include(selTD.TeamsChatsBackupChatScope(sel))
bpc := inject.BackupProducerConfig{ bpc := inject.BackupProducerConfig{
LastBackupVersion: version.NoBackup, LastBackupVersion: version.NoBackup,
Options: ctrlOpts, Options: ctrlOpts,
ProtectedResource: suite.m365.Group.Provider, ProtectedResource: inMock.NewProvider(protectedResource, protectedResource),
Selector: sel.Selector, Selector: sel.Selector,
} }
@ -322,7 +336,7 @@ func (suite *BackupIntgSuite) TestCreateCollections() {
ctx, ctx,
bpc, bpc,
handler, handler,
suite.m365.TenantID, suite.tenantID,
test.scope, test.scope,
func(status *support.ControllerOperationStatus) {}, func(status *support.ControllerOperationStatus) {},
false, false,

View File

@ -23,7 +23,6 @@ import (
"github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/m365/support"
"github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/internal/operations/inject"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/internal/version" "github.com/alcionai/corso/src/internal/version"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
@ -415,7 +414,8 @@ func (suite *ControllerUnitSuite) TestController_CacheItemInfo() {
type ControllerIntegrationSuite struct { type ControllerIntegrationSuite struct {
tester.Suite tester.Suite
ctrl *Controller ctrl *Controller
m365 its.M365IntgTestSetup user string
secondaryUser string
} }
func TestControllerIntegrationSuite(t *testing.T) { func TestControllerIntegrationSuite(t *testing.T) {
@ -428,12 +428,15 @@ func TestControllerIntegrationSuite(t *testing.T) {
func (suite *ControllerIntegrationSuite) SetupSuite() { func (suite *ControllerIntegrationSuite) SetupSuite() {
t := suite.T() t := suite.T()
suite.m365 = its.GetM365(t)
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
suite.ctrl = newController(ctx, t, path.ExchangeService) suite.ctrl = newController(ctx, t, path.ExchangeService)
suite.user = tconfig.M365UserID(t)
suite.secondaryUser = tconfig.SecondaryM365UserID(t)
tester.LogTimeOfTest(t)
} }
func (suite *ControllerIntegrationSuite) TestEmptyCollections() { func (suite *ControllerIntegrationSuite) TestEmptyCollections() {
@ -1061,7 +1064,7 @@ func (suite *ControllerIntegrationSuite) TestRestoreAndBackup_core() {
suite.Run(test.name, func() { suite.Run(test.name, func() {
cfg := stub.ConfigInfo{ cfg := stub.ConfigInfo{
Tenant: suite.ctrl.tenant, Tenant: suite.ctrl.tenant,
ResourceOwners: []string{suite.m365.User.ID}, ResourceOwners: []string{suite.user},
Service: test.service, Service: test.service,
Opts: control.DefaultOptions(), Opts: control.DefaultOptions(),
RestoreCfg: control.DefaultRestoreConfig(dttm.SafeForTesting), RestoreCfg: control.DefaultRestoreConfig(dttm.SafeForTesting),
@ -1140,7 +1143,7 @@ func (suite *ControllerIntegrationSuite) TestMultiFolderBackupDifferentNames() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
restoreSel := getSelectorWith(t, test.service, []string{suite.m365.User.ID}, true) restoreSel := getSelectorWith(t, test.service, []string{suite.user}, true)
expectedDests := make([]destAndCats, 0, len(test.collections)) expectedDests := make([]destAndCats, 0, len(test.collections))
allItems := 0 allItems := 0
allExpectedData := map[string]map[string][]byte{} allExpectedData := map[string]map[string][]byte{}
@ -1151,7 +1154,7 @@ func (suite *ControllerIntegrationSuite) TestMultiFolderBackupDifferentNames() {
restoreCfg.IncludePermissions = true restoreCfg.IncludePermissions = true
expectedDests = append(expectedDests, destAndCats{ expectedDests = append(expectedDests, destAndCats{
resourceOwner: suite.m365.User.ID, resourceOwner: suite.user,
dest: restoreCfg.Location, dest: restoreCfg.Location,
cats: map[path.CategoryType]struct{}{ cats: map[path.CategoryType]struct{}{
collection.Category: {}, collection.Category: {},
@ -1161,7 +1164,7 @@ func (suite *ControllerIntegrationSuite) TestMultiFolderBackupDifferentNames() {
totalItems, _, collections, expectedData, err := stub.CollectionsForInfo( totalItems, _, collections, expectedData, err := stub.CollectionsForInfo(
test.service, test.service,
suite.ctrl.tenant, suite.ctrl.tenant,
suite.m365.User.ID, suite.user,
restoreCfg, restoreCfg,
[]stub.ColInfo{collection}, []stub.ColInfo{collection},
version.Backup) version.Backup)
@ -1286,7 +1289,7 @@ func (suite *ControllerIntegrationSuite) TestRestoreAndBackup_largeMailAttachmen
cfg := stub.ConfigInfo{ cfg := stub.ConfigInfo{
Tenant: suite.ctrl.tenant, Tenant: suite.ctrl.tenant,
ResourceOwners: []string{suite.m365.User.ID}, ResourceOwners: []string{suite.user},
Service: test.service, Service: test.service,
Opts: control.DefaultOptions(), Opts: control.DefaultOptions(),
RestoreCfg: restoreCfg, RestoreCfg: restoreCfg,
@ -1307,7 +1310,7 @@ func (suite *ControllerIntegrationSuite) TestProduceBackupCollections_createsPre
name: "Exchange", name: "Exchange",
resourceCat: resource.Users, resourceCat: resource.Users,
selectorFunc: func(t *testing.T) selectors.Selector { selectorFunc: func(t *testing.T) selectors.Selector {
sel := selectors.NewExchangeBackup([]string{suite.m365.User.ID}) sel := selectors.NewExchangeBackup([]string{suite.user})
sel.Include( sel.Include(
sel.ContactFolders(selectors.None()), sel.ContactFolders(selectors.None()),
sel.EventCalendars(selectors.None()), sel.EventCalendars(selectors.None()),
@ -1326,7 +1329,7 @@ func (suite *ControllerIntegrationSuite) TestProduceBackupCollections_createsPre
name: "OneDrive", name: "OneDrive",
resourceCat: resource.Users, resourceCat: resource.Users,
selectorFunc: func(t *testing.T) selectors.Selector { selectorFunc: func(t *testing.T) selectors.Selector {
sel := selectors.NewOneDriveBackup([]string{suite.m365.User.ID}) sel := selectors.NewOneDriveBackup([]string{suite.user})
sel.Include(sel.Folders(selectors.None())) sel.Include(sel.Folders(selectors.None()))
return sel.Selector return sel.Selector

View File

@ -1,26 +1,31 @@
package m365 package m365
import ( import (
"context"
"fmt" "fmt"
"strings" "strings"
"testing" "testing"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/m365/collection/drive/metadata" "github.com/alcionai/corso/src/internal/m365/collection/drive/metadata"
odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts" odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts"
"github.com/alcionai/corso/src/internal/m365/service/onedrive/stub" "github.com/alcionai/corso/src/internal/m365/service/onedrive/stub"
m365Stub "github.com/alcionai/corso/src/internal/m365/stub" m365Stub "github.com/alcionai/corso/src/internal/m365/stub"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
"github.com/alcionai/corso/src/internal/tester/tconfig" "github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/internal/version" "github.com/alcionai/corso/src/internal/version"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/control/testdata" "github.com/alcionai/corso/src/pkg/control/testdata"
"github.com/alcionai/corso/src/pkg/dttm" "github.com/alcionai/corso/src/pkg/dttm"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/services/m365/api"
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
) )
var ( var (
@ -40,6 +45,119 @@ var (
readPerm = []string{"read"} readPerm = []string{"read"}
) )
func mustGetDefaultDriveID(
t *testing.T,
ctx context.Context, //revive:disable-line:context-as-argument
ac api.Client,
service path.ServiceType,
resourceOwner string,
) string {
var (
err error
d models.Driveable
)
switch service {
case path.OneDriveService:
d, err = ac.Users().GetDefaultDrive(ctx, resourceOwner)
case path.SharePointService:
d, err = ac.Sites().GetDefaultDrive(ctx, resourceOwner)
default:
assert.FailNowf(t, "unknown service type %s", service.String())
}
if err != nil {
err = graph.Wrap(ctx, err, "retrieving drive")
}
require.NoError(t, err, clues.ToCore(err))
id := ptr.Val(d.GetId())
require.NotEmpty(t, id)
return id
}
type suiteInfo interface {
APIClient() api.Client
Tenant() string
// Returns (username, user ID) for the user. These values are used for
// permissions.
PrimaryUser() (string, string)
SecondaryUser() (string, string)
TertiaryUser() (string, string)
// ResourceOwner returns the resource owner to run the backup/restore
// with. This can be different from the values used for permissions and it can
// also be a site.
ResourceOwner() string
Service() path.ServiceType
}
type oneDriveSuite interface {
tester.Suite
suiteInfo
}
type suiteInfoImpl struct {
ac api.Client
controller *Controller
resourceOwner string
secondaryUser string
secondaryUserID string
service path.ServiceType
tertiaryUser string
tertiaryUserID string
user string
userID string
}
func NewSuiteInfoImpl(
t *testing.T,
ctx context.Context, //revive:disable-line:context-as-argument
resourceOwner string,
service path.ServiceType,
) suiteInfoImpl {
ctrl := newController(ctx, t, path.OneDriveService)
return suiteInfoImpl{
ac: ctrl.AC,
controller: ctrl,
resourceOwner: resourceOwner,
secondaryUser: tconfig.SecondaryM365UserID(t),
service: service,
tertiaryUser: tconfig.TertiaryM365UserID(t),
user: tconfig.M365UserID(t),
}
}
func (si suiteInfoImpl) APIClient() api.Client {
return si.ac
}
func (si suiteInfoImpl) Tenant() string {
return si.controller.tenant
}
func (si suiteInfoImpl) PrimaryUser() (string, string) {
return si.user, si.userID
}
func (si suiteInfoImpl) SecondaryUser() (string, string) {
return si.secondaryUser, si.secondaryUserID
}
func (si suiteInfoImpl) TertiaryUser() (string, string) {
return si.tertiaryUser, si.tertiaryUserID
}
func (si suiteInfoImpl) ResourceOwner() string {
return si.resourceOwner
}
func (si suiteInfoImpl) Service() path.ServiceType {
return si.service
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// SharePoint Libraries // SharePoint Libraries
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -49,8 +167,7 @@ var (
type SharePointIntegrationSuite struct { type SharePointIntegrationSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup suiteInfo
resourceAndSvc its.ResourceServicer
} }
func TestSharePointIntegrationSuite(t *testing.T) { func TestSharePointIntegrationSuite(t *testing.T) {
@ -62,38 +179,57 @@ func TestSharePointIntegrationSuite(t *testing.T) {
} }
func (suite *SharePointIntegrationSuite) SetupSuite() { func (suite *SharePointIntegrationSuite) SetupSuite() {
suite.m365 = its.GetM365(suite.T()) t := suite.T()
suite.resourceAndSvc = its.NewResourceService(suite.m365.Site, path.SharePointService)
ctx, flush := tester.NewContext(t)
defer flush()
si := NewSuiteInfoImpl(suite.T(), ctx, tconfig.M365SiteID(suite.T()), path.SharePointService)
// users needed for permissions
user, err := si.controller.AC.Users().GetByID(ctx, si.user, api.CallConfig{})
require.NoError(t, err, "fetching user", si.user, clues.ToCore(err))
si.userID = ptr.Val(user.GetId())
secondaryUser, err := si.controller.AC.Users().GetByID(ctx, si.secondaryUser, api.CallConfig{})
require.NoError(t, err, "fetching user", si.secondaryUser, clues.ToCore(err))
si.secondaryUserID = ptr.Val(secondaryUser.GetId())
tertiaryUser, err := si.controller.AC.Users().GetByID(ctx, si.tertiaryUser, api.CallConfig{})
require.NoError(t, err, "fetching user", si.tertiaryUser, clues.ToCore(err))
si.tertiaryUserID = ptr.Val(tertiaryUser.GetId())
suite.suiteInfo = si
} }
func (suite *SharePointIntegrationSuite) TestRestoreAndBackup_MultipleFilesAndFolders_NoPermissions() { func (suite *SharePointIntegrationSuite) TestRestoreAndBackup_MultipleFilesAndFolders_NoPermissions() {
testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(suite, suite.m365, suite.resourceAndSvc, version.Backup) testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(suite, version.Backup)
} }
// TODO: Re-enable these tests (disabled as it currently acting up CI) // TODO: Re-enable these tests (disabled as it currently acting up CI)
func (suite *SharePointIntegrationSuite) TestPermissionsRestoreAndBackup() { func (suite *SharePointIntegrationSuite) TestPermissionsRestoreAndBackup() {
suite.T().Skip("Temporarily disabled due to CI issues") suite.T().Skip("Temporarily disabled due to CI issues")
testPermissionsRestoreAndBackup(suite, suite.m365, suite.resourceAndSvc, version.Backup) testPermissionsRestoreAndBackup(suite, version.Backup)
} }
func (suite *SharePointIntegrationSuite) TestRestoreNoPermissionsAndBackup() { func (suite *SharePointIntegrationSuite) TestRestoreNoPermissionsAndBackup() {
suite.T().Skip("Temporarily disabled due to CI issues") suite.T().Skip("Temporarily disabled due to CI issues")
testRestoreNoPermissionsAndBackup(suite, suite.m365, suite.resourceAndSvc, version.Backup) testRestoreNoPermissionsAndBackup(suite, version.Backup)
} }
func (suite *SharePointIntegrationSuite) TestPermissionsInheritanceRestoreAndBackup() { func (suite *SharePointIntegrationSuite) TestPermissionsInheritanceRestoreAndBackup() {
suite.T().Skip("Temporarily disabled due to CI issues") suite.T().Skip("Temporarily disabled due to CI issues")
testPermissionsInheritanceRestoreAndBackup(suite, suite.m365, suite.resourceAndSvc, version.Backup) testPermissionsInheritanceRestoreAndBackup(suite, version.Backup)
} }
func (suite *SharePointIntegrationSuite) TestLinkSharesInheritanceRestoreAndBackup() { func (suite *SharePointIntegrationSuite) TestLinkSharesInheritanceRestoreAndBackup() {
suite.T().Skip("Temporarily disabled due to CI issues") suite.T().Skip("Temporarily disabled due to CI issues")
testLinkSharesInheritanceRestoreAndBackup(suite, suite.m365, suite.resourceAndSvc, version.Backup) testLinkSharesInheritanceRestoreAndBackup(suite, version.Backup)
} }
func (suite *SharePointIntegrationSuite) TestRestoreFolderNamedFolderRegression() { func (suite *SharePointIntegrationSuite) TestRestoreFolderNamedFolderRegression() {
// No reason why it couldn't work with previous versions, but this is when it got introduced. // No reason why it couldn't work with previous versions, but this is when it got introduced.
testRestoreFolderNamedFolderRegression(suite, suite.m365, suite.resourceAndSvc, version.All8MigrateUserPNToID) testRestoreFolderNamedFolderRegression(suite, version.All8MigrateUserPNToID)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -101,8 +237,7 @@ func (suite *SharePointIntegrationSuite) TestRestoreFolderNamedFolderRegression(
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
type OneDriveIntegrationSuite struct { type OneDriveIntegrationSuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup suiteInfo
resourceAndSvc its.ResourceServicer
} }
func TestOneDriveIntegrationSuite(t *testing.T) { func TestOneDriveIntegrationSuite(t *testing.T) {
@ -114,33 +249,51 @@ func TestOneDriveIntegrationSuite(t *testing.T) {
} }
func (suite *OneDriveIntegrationSuite) SetupSuite() { func (suite *OneDriveIntegrationSuite) SetupSuite() {
suite.m365 = its.GetM365(suite.T()) t := suite.T()
suite.resourceAndSvc = its.NewResourceService(suite.m365.User, path.OneDriveService)
ctx, flush := tester.NewContext(t)
defer flush()
si := NewSuiteInfoImpl(t, ctx, tconfig.M365UserID(t), path.OneDriveService)
user, err := si.controller.AC.Users().GetByID(ctx, si.user, api.CallConfig{})
require.NoError(t, err, "fetching user", si.user, clues.ToCore(err))
si.userID = ptr.Val(user.GetId())
secondaryUser, err := si.controller.AC.Users().GetByID(ctx, si.secondaryUser, api.CallConfig{})
require.NoError(t, err, "fetching user", si.secondaryUser, clues.ToCore(err))
si.secondaryUserID = ptr.Val(secondaryUser.GetId())
tertiaryUser, err := si.controller.AC.Users().GetByID(ctx, si.tertiaryUser, api.CallConfig{})
require.NoError(t, err, "fetching user", si.tertiaryUser, clues.ToCore(err))
si.tertiaryUserID = ptr.Val(tertiaryUser.GetId())
suite.suiteInfo = si
} }
func (suite *OneDriveIntegrationSuite) TestRestoreAndBackup_MultipleFilesAndFolders_NoPermissions() { func (suite *OneDriveIntegrationSuite) TestRestoreAndBackup_MultipleFilesAndFolders_NoPermissions() {
testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(suite, suite.m365, suite.resourceAndSvc, version.Backup) testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(suite, version.Backup)
} }
func (suite *OneDriveIntegrationSuite) TestPermissionsRestoreAndBackup() { func (suite *OneDriveIntegrationSuite) TestPermissionsRestoreAndBackup() {
testPermissionsRestoreAndBackup(suite, suite.m365, suite.resourceAndSvc, version.Backup) testPermissionsRestoreAndBackup(suite, version.Backup)
} }
func (suite *OneDriveIntegrationSuite) TestRestoreNoPermissionsAndBackup() { func (suite *OneDriveIntegrationSuite) TestRestoreNoPermissionsAndBackup() {
testRestoreNoPermissionsAndBackup(suite, suite.m365, suite.resourceAndSvc, version.Backup) testRestoreNoPermissionsAndBackup(suite, version.Backup)
} }
func (suite *OneDriveIntegrationSuite) TestPermissionsInheritanceRestoreAndBackup() { func (suite *OneDriveIntegrationSuite) TestPermissionsInheritanceRestoreAndBackup() {
testPermissionsInheritanceRestoreAndBackup(suite, suite.m365, suite.resourceAndSvc, version.Backup) testPermissionsInheritanceRestoreAndBackup(suite, version.Backup)
} }
func (suite *OneDriveIntegrationSuite) TestLinkSharesInheritanceRestoreAndBackup() { func (suite *OneDriveIntegrationSuite) TestLinkSharesInheritanceRestoreAndBackup() {
testLinkSharesInheritanceRestoreAndBackup(suite, suite.m365, suite.resourceAndSvc, version.Backup) testLinkSharesInheritanceRestoreAndBackup(suite, version.Backup)
} }
func (suite *OneDriveIntegrationSuite) TestRestoreFolderNamedFolderRegression() { func (suite *OneDriveIntegrationSuite) TestRestoreFolderNamedFolderRegression() {
// No reason why it couldn't work with previous versions, but this is when it got introduced. // No reason why it couldn't work with previous versions, but this is when it got introduced.
testRestoreFolderNamedFolderRegression(suite, suite.m365, suite.resourceAndSvc, version.All8MigrateUserPNToID) testRestoreFolderNamedFolderRegression(suite, version.All8MigrateUserPNToID)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -148,8 +301,7 @@ func (suite *OneDriveIntegrationSuite) TestRestoreFolderNamedFolderRegression()
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
type OneDriveNightlySuite struct { type OneDriveNightlySuite struct {
tester.Suite tester.Suite
m365 its.M365IntgTestSetup suiteInfo
resourceAndSvc its.ResourceServicer
} }
func TestOneDriveNightlySuite(t *testing.T) { func TestOneDriveNightlySuite(t *testing.T) {
@ -161,48 +313,70 @@ func TestOneDriveNightlySuite(t *testing.T) {
} }
func (suite *OneDriveNightlySuite) SetupSuite() { func (suite *OneDriveNightlySuite) SetupSuite() {
suite.m365 = its.GetM365(suite.T()) t := suite.T()
suite.resourceAndSvc = its.NewResourceService(suite.m365.User, path.OneDriveService)
ctx, flush := tester.NewContext(t)
defer flush()
si := NewSuiteInfoImpl(t, ctx, tconfig.M365UserID(t), path.OneDriveService)
user, err := si.controller.AC.Users().GetByID(ctx, si.user, api.CallConfig{})
require.NoError(t, err, "fetching user", si.user, clues.ToCore(err))
si.userID = ptr.Val(user.GetId())
secondaryUser, err := si.controller.AC.Users().GetByID(ctx, si.secondaryUser, api.CallConfig{})
require.NoError(t, err, "fetching user", si.secondaryUser, clues.ToCore(err))
si.secondaryUserID = ptr.Val(secondaryUser.GetId())
tertiaryUser, err := si.controller.AC.Users().GetByID(ctx, si.tertiaryUser, api.CallConfig{})
require.NoError(t, err, "fetching user", si.tertiaryUser, clues.ToCore(err))
si.tertiaryUserID = ptr.Val(tertiaryUser.GetId())
suite.suiteInfo = si
} }
func (suite *OneDriveNightlySuite) TestRestoreAndBackup_MultipleFilesAndFolders_NoPermissions() { func (suite *OneDriveNightlySuite) TestRestoreAndBackup_MultipleFilesAndFolders_NoPermissions() {
testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(suite, suite.m365, suite.resourceAndSvc, 0) testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(suite, 0)
} }
func (suite *OneDriveNightlySuite) TestPermissionsRestoreAndBackup() { func (suite *OneDriveNightlySuite) TestPermissionsRestoreAndBackup() {
testPermissionsRestoreAndBackup(suite, suite.m365, suite.resourceAndSvc, version.OneDrive1DataAndMetaFiles) testPermissionsRestoreAndBackup(suite, version.OneDrive1DataAndMetaFiles)
} }
func (suite *OneDriveNightlySuite) TestRestoreNoPermissionsAndBackup() { func (suite *OneDriveNightlySuite) TestRestoreNoPermissionsAndBackup() {
testRestoreNoPermissionsAndBackup(suite, suite.m365, suite.resourceAndSvc, version.OneDrive1DataAndMetaFiles) testRestoreNoPermissionsAndBackup(suite, version.OneDrive1DataAndMetaFiles)
} }
func (suite *OneDriveNightlySuite) TestPermissionsInheritanceRestoreAndBackup() { func (suite *OneDriveNightlySuite) TestPermissionsInheritanceRestoreAndBackup() {
// No reason why it couldn't work with previous versions, but this is when it got introduced. // No reason why it couldn't work with previous versions, but this is when it got introduced.
testPermissionsInheritanceRestoreAndBackup( testPermissionsInheritanceRestoreAndBackup(suite, version.OneDrive4DirIncludesPermissions)
suite,
suite.m365,
suite.resourceAndSvc,
version.OneDrive4DirIncludesPermissions)
} }
func (suite *OneDriveNightlySuite) TestLinkSharesInheritanceRestoreAndBackup() { func (suite *OneDriveNightlySuite) TestLinkSharesInheritanceRestoreAndBackup() {
testLinkSharesInheritanceRestoreAndBackup(suite, suite.m365, suite.resourceAndSvc, version.Backup) testLinkSharesInheritanceRestoreAndBackup(suite, version.Backup)
} }
func (suite *OneDriveNightlySuite) TestRestoreFolderNamedFolderRegression() { func (suite *OneDriveNightlySuite) TestRestoreFolderNamedFolderRegression() {
// No reason why it couldn't work with previous versions, but this is when it got introduced. // No reason why it couldn't work with previous versions, but this is when it got introduced.
testRestoreFolderNamedFolderRegression(suite, suite.m365, suite.resourceAndSvc, version.All8MigrateUserPNToID) testRestoreFolderNamedFolderRegression(suite, version.All8MigrateUserPNToID)
} }
func testRestoreAndBackupMultipleFilesAndFoldersNoPermissions( func testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(
suite tester.Suite, suite oneDriveSuite,
m365 its.M365IntgTestSetup,
resourceAndSvc its.ResourceServicer,
startVersion int, startVersion int,
) { ) {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
// Get the default drive ID for the test user. // Get the default drive ID for the test user.
driveID := resourceAndSvc.Resource().DriveID driveID := mustGetDefaultDriveID(
t,
ctx,
suite.APIClient(),
suite.Service(),
suite.ResourceOwner())
rootPath := []string{ rootPath := []string{
odConsts.DrivesPathDir, odConsts.DrivesPathDir,
@ -315,17 +489,17 @@ func testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(
}, },
} }
expected, err := stub.DataForInfo(resourceAndSvc.Service(), cols, version.Backup) expected, err := stub.DataForInfo(suite.Service(), cols, version.Backup)
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
for vn := startVersion; vn <= version.Backup; vn++ { for vn := startVersion; vn <= version.Backup; vn++ {
suite.Run(fmt.Sprintf("Version%d", vn), func() { suite.Run(fmt.Sprintf("Version%d", vn), func() {
t := suite.T() t := suite.T()
input, err := stub.DataForInfo(resourceAndSvc.Service(), cols, vn) input, err := stub.DataForInfo(suite.Service(), cols, vn)
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
testData := restoreBackupInfoMultiVersion{ testData := restoreBackupInfoMultiVersion{
service: resourceAndSvc.Service(), service: suite.Service(),
backupVersion: vn, backupVersion: vn,
collectionsPrevious: input, collectionsPrevious: input,
collectionsLatest: expected, collectionsLatest: expected,
@ -338,8 +512,8 @@ func testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(
opts := control.DefaultOptions() opts := control.DefaultOptions()
cfg := m365Stub.ConfigInfo{ cfg := m365Stub.ConfigInfo{
Tenant: m365.TenantID, Tenant: suite.Tenant(),
ResourceOwners: []string{resourceAndSvc.Resource().ID}, ResourceOwners: []string{suite.ResourceOwner()},
Service: testData.service, Service: testData.service,
Opts: opts, Opts: opts,
RestoreCfg: restoreCfg, RestoreCfg: restoreCfg,
@ -350,14 +524,21 @@ func testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(
} }
} }
func testPermissionsRestoreAndBackup( func testPermissionsRestoreAndBackup(suite oneDriveSuite, startVersion int) {
suite tester.Suite, t := suite.T()
m365 its.M365IntgTestSetup,
resourceAndSvc its.ResourceServicer, ctx, flush := tester.NewContext(t)
startVersion int, defer flush()
) {
secondaryUserName, secondaryUserID := suite.SecondaryUser()
// Get the default drive ID for the test user. // Get the default drive ID for the test user.
driveID := resourceAndSvc.Resource().DriveID driveID := mustGetDefaultDriveID(
t,
ctx,
suite.APIClient(),
suite.Service(),
suite.ResourceOwner())
fileName2 := "test-file2.txt" fileName2 := "test-file2.txt"
folderCName := "folder-c" folderCName := "folder-c"
@ -407,8 +588,8 @@ func testPermissionsRestoreAndBackup(
Data: fileAData, Data: fileAData,
Meta: stub.MetaData{ Meta: stub.MetaData{
Perms: stub.PermData{ Perms: stub.PermData{
User: m365.SecondaryUser.Email, User: secondaryUserName,
EntityID: m365.SecondaryUser.ID, EntityID: secondaryUserID,
Roles: writePerm, Roles: writePerm,
}, },
}, },
@ -434,8 +615,8 @@ func testPermissionsRestoreAndBackup(
Name: folderAName, Name: folderAName,
Meta: stub.MetaData{ Meta: stub.MetaData{
Perms: stub.PermData{ Perms: stub.PermData{
User: m365.SecondaryUser.Email, User: secondaryUserName,
EntityID: m365.SecondaryUser.ID, EntityID: secondaryUserID,
Roles: readPerm, Roles: readPerm,
}, },
}, },
@ -444,8 +625,8 @@ func testPermissionsRestoreAndBackup(
Name: folderCName, Name: folderCName,
Meta: stub.MetaData{ Meta: stub.MetaData{
Perms: stub.PermData{ Perms: stub.PermData{
User: m365.SecondaryUser.Email, User: secondaryUserName,
EntityID: m365.SecondaryUser.ID, EntityID: secondaryUserID,
Roles: readPerm, Roles: readPerm,
}, },
}, },
@ -465,8 +646,8 @@ func testPermissionsRestoreAndBackup(
Data: fileBData, Data: fileBData,
Meta: stub.MetaData{ Meta: stub.MetaData{
Perms: stub.PermData{ Perms: stub.PermData{
User: m365.SecondaryUser.Email, User: secondaryUserName,
EntityID: m365.SecondaryUser.ID, EntityID: secondaryUserID,
Roles: writePerm, Roles: writePerm,
}, },
}, },
@ -477,8 +658,8 @@ func testPermissionsRestoreAndBackup(
Name: folderAName, Name: folderAName,
Meta: stub.MetaData{ Meta: stub.MetaData{
Perms: stub.PermData{ Perms: stub.PermData{
User: m365.SecondaryUser.Email, User: secondaryUserName,
EntityID: m365.SecondaryUser.ID, EntityID: secondaryUserID,
Roles: readPerm, Roles: readPerm,
}, },
}, },
@ -496,15 +677,15 @@ func testPermissionsRestoreAndBackup(
// name: fileName, // name: fileName,
// data: fileDData, // data: fileDData,
// perms: stub.PermData{ // perms: stub.PermData{
// user: m365.SecondaryUser.Email, // user: secondaryUserName,
// entityID: m365.SecondaryUser.ID, // entityID: secondaryUserID,
// roles: readPerm, // roles: readPerm,
// }, // },
// }, // },
// }, // },
// Perms: stub.PermData{ // Perms: stub.PermData{
// User: m365.SecondaryUser.Email, // User: secondaryUserName,
// EntityID: m365.SecondaryUser.ID, // EntityID: secondaryUserID,
// Roles: readPerm, // Roles: readPerm,
// }, // },
// }, // },
@ -518,8 +699,8 @@ func testPermissionsRestoreAndBackup(
Data: fileEData, Data: fileEData,
Meta: stub.MetaData{ Meta: stub.MetaData{
Perms: stub.PermData{ Perms: stub.PermData{
User: m365.SecondaryUser.Email, User: secondaryUserName,
EntityID: m365.SecondaryUser.ID, EntityID: secondaryUserID,
Roles: writePerm, Roles: writePerm,
}, },
}, },
@ -527,8 +708,8 @@ func testPermissionsRestoreAndBackup(
}, },
Meta: stub.MetaData{ Meta: stub.MetaData{
Perms: stub.PermData{ Perms: stub.PermData{
User: m365.SecondaryUser.Email, User: secondaryUserName,
EntityID: m365.SecondaryUser.ID, EntityID: secondaryUserID,
Roles: readPerm, Roles: readPerm,
}, },
}, },
@ -548,18 +729,17 @@ func testPermissionsRestoreAndBackup(
}, },
Meta: stub.MetaData{ Meta: stub.MetaData{
Perms: stub.PermData{ Perms: stub.PermData{
User: m365.SecondaryUser.Email, User: secondaryUserName,
EntityID: m365.SecondaryUser.ID, EntityID: secondaryUserID,
Roles: readPerm, Roles: readPerm,
}, },
}, },
}, },
} }
expected, err := stub.DataForInfo(resourceAndSvc.Service(), cols, version.Backup) expected, err := stub.DataForInfo(suite.Service(), cols, version.Backup)
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
bss := suite.Service().String()
bss := resourceAndSvc.Service().String()
for vn := startVersion; vn <= version.Backup; vn++ { for vn := startVersion; vn <= version.Backup; vn++ {
suite.Run(fmt.Sprintf("%s-Version%d", bss, vn), func() { suite.Run(fmt.Sprintf("%s-Version%d", bss, vn), func() {
@ -567,11 +747,11 @@ func testPermissionsRestoreAndBackup(
// Ideally this can always be true or false and still // Ideally this can always be true or false and still
// work, but limiting older versions to use emails so as // work, but limiting older versions to use emails so as
// to validate that flow as well. // to validate that flow as well.
input, err := stub.DataForInfo(resourceAndSvc.Service(), cols, vn) input, err := stub.DataForInfo(suite.Service(), cols, vn)
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
testData := restoreBackupInfoMultiVersion{ testData := restoreBackupInfoMultiVersion{
service: resourceAndSvc.Service(), service: suite.Service(),
backupVersion: vn, backupVersion: vn,
collectionsPrevious: input, collectionsPrevious: input,
collectionsLatest: expected, collectionsLatest: expected,
@ -584,8 +764,8 @@ func testPermissionsRestoreAndBackup(
opts := control.DefaultOptions() opts := control.DefaultOptions()
cfg := m365Stub.ConfigInfo{ cfg := m365Stub.ConfigInfo{
Tenant: m365.TenantID, Tenant: suite.Tenant(),
ResourceOwners: []string{resourceAndSvc.Resource().ID}, ResourceOwners: []string{suite.ResourceOwner()},
Service: testData.service, Service: testData.service,
Opts: opts, Opts: opts,
RestoreCfg: restoreCfg, RestoreCfg: restoreCfg,
@ -596,14 +776,21 @@ func testPermissionsRestoreAndBackup(
} }
} }
func testRestoreNoPermissionsAndBackup( func testRestoreNoPermissionsAndBackup(suite oneDriveSuite, startVersion int) {
suite tester.Suite, t := suite.T()
m365 its.M365IntgTestSetup,
resourceAndSvc its.ResourceServicer, ctx, flush := tester.NewContext(t)
startVersion int, defer flush()
) {
secondaryUserName, secondaryUserID := suite.SecondaryUser()
// Get the default drive ID for the test user. // Get the default drive ID for the test user.
driveID := resourceAndSvc.Resource().DriveID driveID := mustGetDefaultDriveID(
t,
ctx,
suite.APIClient(),
suite.Service(),
suite.ResourceOwner())
inputCols := []stub.ColInfo{ inputCols := []stub.ColInfo{
{ {
@ -618,8 +805,8 @@ func testRestoreNoPermissionsAndBackup(
Data: fileAData, Data: fileAData,
Meta: stub.MetaData{ Meta: stub.MetaData{
Perms: stub.PermData{ Perms: stub.PermData{
User: m365.SecondaryUser.Email, User: secondaryUserName,
EntityID: m365.SecondaryUser.ID, EntityID: secondaryUserID,
Roles: writePerm, Roles: writePerm,
}, },
SharingMode: metadata.SharingModeCustom, SharingMode: metadata.SharingModeCustom,
@ -646,20 +833,18 @@ func testRestoreNoPermissionsAndBackup(
}, },
} }
expected, err := stub.DataForInfo(resourceAndSvc.Service(), expectedCols, version.Backup) expected, err := stub.DataForInfo(suite.Service(), expectedCols, version.Backup)
require.NoError(suite.T(), err, clues.ToCore(err)) require.NoError(suite.T(), err)
bss := suite.Service().String()
bss := resourceAndSvc.Service().String()
for vn := startVersion; vn <= version.Backup; vn++ { for vn := startVersion; vn <= version.Backup; vn++ {
suite.Run(fmt.Sprintf("%s-Version%d", bss, vn), func() { suite.Run(fmt.Sprintf("%s-Version%d", bss, vn), func() {
t := suite.T() t := suite.T()
input, err := stub.DataForInfo(suite.Service(), inputCols, vn)
input, err := stub.DataForInfo(resourceAndSvc.Service(), inputCols, vn) require.NoError(suite.T(), err)
require.NoError(t, err, clues.ToCore(err))
testData := restoreBackupInfoMultiVersion{ testData := restoreBackupInfoMultiVersion{
service: resourceAndSvc.Service(), service: suite.Service(),
backupVersion: vn, backupVersion: vn,
collectionsPrevious: input, collectionsPrevious: input,
collectionsLatest: expected, collectionsLatest: expected,
@ -672,8 +857,8 @@ func testRestoreNoPermissionsAndBackup(
opts := control.DefaultOptions() opts := control.DefaultOptions()
cfg := m365Stub.ConfigInfo{ cfg := m365Stub.ConfigInfo{
Tenant: m365.TenantID, Tenant: suite.Tenant(),
ResourceOwners: []string{resourceAndSvc.Resource().ID}, ResourceOwners: []string{suite.ResourceOwner()},
Service: testData.service, Service: testData.service,
Opts: opts, Opts: opts,
RestoreCfg: restoreCfg, RestoreCfg: restoreCfg,
@ -686,14 +871,22 @@ func testRestoreNoPermissionsAndBackup(
// This is similar to TestPermissionsRestoreAndBackup but tests purely // This is similar to TestPermissionsRestoreAndBackup but tests purely
// for inheritance and that too only with newer versions // for inheritance and that too only with newer versions
func testPermissionsInheritanceRestoreAndBackup( func testPermissionsInheritanceRestoreAndBackup(suite oneDriveSuite, startVersion int) {
suite tester.Suite, t := suite.T()
m365 its.M365IntgTestSetup,
resourceAndSvc its.ResourceServicer, ctx, flush := tester.NewContext(t)
startVersion int, defer flush()
) {
secondaryUserName, secondaryUserID := suite.SecondaryUser()
tertiaryUserName, tertiaryUserID := suite.TertiaryUser()
// Get the default drive ID for the test user. // Get the default drive ID for the test user.
driveID := resourceAndSvc.Resource().DriveID driveID := mustGetDefaultDriveID(
t,
ctx,
suite.APIClient(),
suite.Service(),
suite.ResourceOwner())
folderAName := "custom" folderAName := "custom"
folderBName := "inherited" folderBName := "inherited"
@ -737,8 +930,8 @@ func testPermissionsInheritanceRestoreAndBackup(
Data: fileAData, Data: fileAData,
Meta: stub.MetaData{ Meta: stub.MetaData{
Perms: stub.PermData{ Perms: stub.PermData{
User: m365.SecondaryUser.Email, User: secondaryUserName,
EntityID: m365.SecondaryUser.ID, EntityID: secondaryUserID,
Roles: writePerm, Roles: writePerm,
}, },
SharingMode: metadata.SharingModeCustom, SharingMode: metadata.SharingModeCustom,
@ -810,8 +1003,8 @@ func testPermissionsInheritanceRestoreAndBackup(
}, },
Meta: stub.MetaData{ Meta: stub.MetaData{
Perms: stub.PermData{ Perms: stub.PermData{
User: m365.TertiaryUser.Email, User: tertiaryUserName,
EntityID: m365.TertiaryUser.ID, EntityID: tertiaryUserID,
Roles: readPerm, Roles: readPerm,
}, },
SharingMode: metadata.SharingModeCustom, SharingMode: metadata.SharingModeCustom,
@ -822,8 +1015,8 @@ func testPermissionsInheritanceRestoreAndBackup(
Files: fileSet, Files: fileSet,
Meta: stub.MetaData{ Meta: stub.MetaData{
Perms: stub.PermData{ Perms: stub.PermData{
User: m365.TertiaryUser.Email, User: tertiaryUserName,
EntityID: m365.TertiaryUser.ID, EntityID: tertiaryUserID,
Roles: writePerm, Roles: writePerm,
}, },
SharingMode: metadata.SharingModeCustom, SharingMode: metadata.SharingModeCustom,
@ -845,10 +1038,9 @@ func testPermissionsInheritanceRestoreAndBackup(
}, },
} }
expected, err := stub.DataForInfo(resourceAndSvc.Service(), cols, version.Backup) expected, err := stub.DataForInfo(suite.Service(), cols, version.Backup)
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
bss := suite.Service().String()
bss := resourceAndSvc.Service().String()
for vn := startVersion; vn <= version.Backup; vn++ { for vn := startVersion; vn <= version.Backup; vn++ {
suite.Run(fmt.Sprintf("%s-Version%d", bss, vn), func() { suite.Run(fmt.Sprintf("%s-Version%d", bss, vn), func() {
@ -856,11 +1048,11 @@ func testPermissionsInheritanceRestoreAndBackup(
// Ideally this can always be true or false and still // Ideally this can always be true or false and still
// work, but limiting older versions to use emails so as // work, but limiting older versions to use emails so as
// to validate that flow as well. // to validate that flow as well.
input, err := stub.DataForInfo(resourceAndSvc.Service(), cols, vn) input, err := stub.DataForInfo(suite.Service(), cols, vn)
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
testData := restoreBackupInfoMultiVersion{ testData := restoreBackupInfoMultiVersion{
service: resourceAndSvc.Service(), service: suite.Service(),
backupVersion: vn, backupVersion: vn,
collectionsPrevious: input, collectionsPrevious: input,
collectionsLatest: expected, collectionsLatest: expected,
@ -873,8 +1065,8 @@ func testPermissionsInheritanceRestoreAndBackup(
opts := control.DefaultOptions() opts := control.DefaultOptions()
cfg := m365Stub.ConfigInfo{ cfg := m365Stub.ConfigInfo{
Tenant: m365.TenantID, Tenant: suite.Tenant(),
ResourceOwners: []string{resourceAndSvc.Resource().ID}, ResourceOwners: []string{suite.ResourceOwner()},
Service: testData.service, Service: testData.service,
Opts: opts, Opts: opts,
RestoreCfg: restoreCfg, RestoreCfg: restoreCfg,
@ -885,26 +1077,33 @@ func testPermissionsInheritanceRestoreAndBackup(
} }
} }
func testLinkSharesInheritanceRestoreAndBackup( func testLinkSharesInheritanceRestoreAndBackup(suite oneDriveSuite, startVersion int) {
suite tester.Suite, t := suite.T()
m365 its.M365IntgTestSetup,
resourceAndSvc its.ResourceServicer, ctx, flush := tester.NewContext(t)
startVersion int, defer flush()
) {
secondaryUserName, secondaryUserID := suite.SecondaryUser()
secondaryUser := metadata.Entity{ secondaryUser := metadata.Entity{
ID: m365.SecondaryUser.ID, ID: secondaryUserID,
Email: m365.SecondaryUser.Email, Email: secondaryUserName,
EntityType: metadata.GV2User, EntityType: metadata.GV2User,
} }
tertiaryUserName, tertiaryUserID := suite.TertiaryUser()
tertiaryUser := metadata.Entity{ tertiaryUser := metadata.Entity{
ID: m365.TertiaryUser.ID, ID: tertiaryUserID,
Email: m365.TertiaryUser.Email, Email: tertiaryUserName,
EntityType: metadata.GV2User, EntityType: metadata.GV2User,
} }
// Get the default drive ID for the test user. // Get the default drive ID for the test user.
driveID := resourceAndSvc.Resource().DriveID driveID := mustGetDefaultDriveID(
t,
ctx,
suite.APIClient(),
suite.Service(),
suite.ResourceOwner())
folderAName := "custom" folderAName := "custom"
folderBName := "inherited" folderBName := "inherited"
@ -1048,10 +1247,9 @@ func testLinkSharesInheritanceRestoreAndBackup(
}, },
} }
expected, err := stub.DataForInfo(resourceAndSvc.Service(), cols, version.Backup) expected, err := stub.DataForInfo(suite.Service(), cols, version.Backup)
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
bss := suite.Service().String()
bss := resourceAndSvc.Service().String()
for vn := startVersion; vn <= version.Backup; vn++ { for vn := startVersion; vn <= version.Backup; vn++ {
suite.Run(fmt.Sprintf("%s-Version%d", bss, vn), func() { suite.Run(fmt.Sprintf("%s-Version%d", bss, vn), func() {
@ -1059,11 +1257,11 @@ func testLinkSharesInheritanceRestoreAndBackup(
// Ideally this can always be true or false and still // Ideally this can always be true or false and still
// work, but limiting older versions to use emails so as // work, but limiting older versions to use emails so as
// to validate that flow as well. // to validate that flow as well.
input, err := stub.DataForInfo(resourceAndSvc.Service(), cols, vn) input, err := stub.DataForInfo(suite.Service(), cols, vn)
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
testData := restoreBackupInfoMultiVersion{ testData := restoreBackupInfoMultiVersion{
service: resourceAndSvc.Service(), service: suite.Service(),
backupVersion: vn, backupVersion: vn,
collectionsPrevious: input, collectionsPrevious: input,
collectionsLatest: expected, collectionsLatest: expected,
@ -1076,8 +1274,8 @@ func testLinkSharesInheritanceRestoreAndBackup(
opts := control.DefaultOptions() opts := control.DefaultOptions()
cfg := m365Stub.ConfigInfo{ cfg := m365Stub.ConfigInfo{
Tenant: m365.TenantID, Tenant: suite.Tenant(),
ResourceOwners: []string{resourceAndSvc.Resource().ID}, ResourceOwners: []string{suite.ResourceOwner()},
Service: testData.service, Service: testData.service,
Opts: opts, Opts: opts,
RestoreCfg: restoreCfg, RestoreCfg: restoreCfg,
@ -1089,13 +1287,21 @@ func testLinkSharesInheritanceRestoreAndBackup(
} }
func testRestoreFolderNamedFolderRegression( func testRestoreFolderNamedFolderRegression(
suite tester.Suite, suite oneDriveSuite,
m365 its.M365IntgTestSetup,
resourceAndSvc its.ResourceServicer,
startVersion int, startVersion int,
) { ) {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
// Get the default drive ID for the test user. // Get the default drive ID for the test user.
driveID := resourceAndSvc.Resource().DriveID driveID := mustGetDefaultDriveID(
suite.T(),
ctx,
suite.APIClient(),
suite.Service(),
suite.ResourceOwner())
rootPath := []string{ rootPath := []string{
odConsts.DrivesPathDir, odConsts.DrivesPathDir,
@ -1164,19 +1370,18 @@ func testRestoreFolderNamedFolderRegression(
}, },
} }
expected, err := stub.DataForInfo(resourceAndSvc.Service(), cols, version.Backup) expected, err := stub.DataForInfo(suite.Service(), cols, version.Backup)
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
bss := suite.Service().String()
bss := resourceAndSvc.Service().String()
for vn := startVersion; vn <= version.Backup; vn++ { for vn := startVersion; vn <= version.Backup; vn++ {
suite.Run(fmt.Sprintf("%s-Version%d", bss, vn), func() { suite.Run(fmt.Sprintf("%s-Version%d", bss, vn), func() {
t := suite.T() t := suite.T()
input, err := stub.DataForInfo(resourceAndSvc.Service(), cols, vn) input, err := stub.DataForInfo(suite.Service(), cols, vn)
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
testData := restoreBackupInfoMultiVersion{ testData := restoreBackupInfoMultiVersion{
service: resourceAndSvc.Service(), service: suite.Service(),
backupVersion: vn, backupVersion: vn,
collectionsPrevious: input, collectionsPrevious: input,
collectionsLatest: expected, collectionsLatest: expected,
@ -1188,8 +1393,8 @@ func testRestoreFolderNamedFolderRegression(
opts := control.DefaultOptions() opts := control.DefaultOptions()
cfg := m365Stub.ConfigInfo{ cfg := m365Stub.ConfigInfo{
Tenant: m365.TenantID, Tenant: suite.Tenant(),
ResourceOwners: []string{resourceAndSvc.Resource().ID}, ResourceOwners: []string{suite.ResourceOwner()},
Service: testData.service, Service: testData.service,
Opts: opts, Opts: opts,
RestoreCfg: restoreCfg, RestoreCfg: restoreCfg,

Some files were not shown because too many files have changed in this diff Show More