Repository: thelastpickle/cassandra-medusa

Issue:
Make gRPC BackupStatus() operation be about the current node only.
[Project board link](https://github.com/orgs/k8ssandra/projects/8/views/1?pane=issue&itemId=40584156)

[The gGRPC's BackupStatus()](https://github.com/thelastpickle/cassandra-medusa/blob/master/medusa/service/grpc/server.py#L166) currently works atop cluster-wide backups. Considering we use the this in the context of the k8ssandra-operator, where we need to determine a status of a single/current node, this is inaccurate.

We need to make the `BackupStatus()` return a status of a specific backup for the current node.

So instead of:
```
backup = storage.get_cluster_backup(request.backupName)
```

We need to do something like:
```
backup = storage.get_node_backup(storage_config.fqdn, request.backupName)
```

We should also call `update_backup_status` from `set_backup_result` so that the status is updated in `BackupMan` when the backup finishes or fails.

Discussion:
github-actions[bot]: No linked issues found. Please add the corresponding issues in the pull request description. <br/> [Use GitHub automation to close the issue when a PR is merged](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) <!-- metadata = {"action":"linked_issue"} -->
sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=thelastpickle_cassandra-medusa&pullRequest=681)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=SECURITY_HOTSPOT) [![E](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/E-16px.png 'E')](https://sonarcloud.io/project/security_hotspots?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=SECURITY_HOTSPOT) [3 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=thelastpickle_cassandra-medusa&pullRequest=681) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=thelastpickle_cassandra-medusa&pullRequest=681&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=thelastpickle_cassandra-medusa&pullRequest=681&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)
sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=thelastpickle_cassandra-medusa&pullRequest=681)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=SECURITY_HOTSPOT) [![E](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/E-16px.png 'E')](https://sonarcloud.io/project/security_hotspots?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=SECURITY_HOTSPOT) [3 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=thelastpickle_cassandra-medusa&pullRequest=681) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=thelastpickle_cassandra-medusa&pullRequest=681&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=thelastpickle_cassandra-medusa&pullRequest=681&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/thelastpickle/cassandra-medusa/pull/681?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thelastpickle) Report
> Merging [#681](https://app.codecov.io/gh/thelastpickle/cassandra-medusa/pull/681?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thelastpickle) (c3371db) into [master](https://app.codecov.io/gh/thelastpickle/cassandra-medusa/commit/2d79dff45dfccca40067ddefe86c46586287f02c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thelastpickle) (2d79dff) will **decrease** coverage by `1.49%`.
> Report is 1 commits behind head on master.
> The diff coverage is `100.00%`.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/thelastpickle/cassandra-medusa/pull/681/graphs/tree.svg?width=650&height=150&src=pr&token=KTDCRD82NU&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thelastpickle)](https://app.codecov.io/gh/thelastpickle/cassandra-medusa/pull/681?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thelastpickle)

```diff
@@            Coverage Diff             @@
##           master     #681      +/-   ##
==========================================
- Coverage   79.45%   77.96%   -1.49%     
==========================================
  Files          52       52              
  Lines        4516     4515       -1     
  Branches      919      918       -1     
==========================================
- Hits         3588     3520      -68     
- Misses        896      963      +67     
  Partials       32       32              
```


| [Files](https://app.codecov.io/gh/thelastpickle/cassandra-medusa/pull/681?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thelastpickle) | Coverage Δ | |
|---|---|---|
| [medusa/service/grpc/server.py](https://app.codecov.io/gh/thelastpickle/cassandra-medusa/pull/681?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thelastpickle#diff-bWVkdXNhL3NlcnZpY2UvZ3JwYy9zZXJ2ZXIucHk=) | `80.16% <100.00%> (+1.15%)` | :arrow_up: |

... and [3 files with indirect coverage changes](https://app.codecov.io/gh/thelastpickle/cassandra-medusa/pull/681/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thelastpickle)

</details>
sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=thelastpickle_cassandra-medusa&pullRequest=681)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=SECURITY_HOTSPOT) [![E](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/E-16px.png 'E')](https://sonarcloud.io/project/security_hotspots?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=SECURITY_HOTSPOT) [3 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=thelastpickle_cassandra-medusa&pullRequest=681) No Coverage information  
[![10.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/20-16px.png '10.7%')](https://sonarcloud.io/component_measures?id=thelastpickle_cassandra-medusa&pullRequest=681&metric=new_duplicated_lines_density&view=list) [10.7% Duplication](https://sonarcloud.io/component_measures?id=thelastpickle_cassandra-medusa&pullRequest=681&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)
sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=thelastpickle_cassandra-medusa&pullRequest=681)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=SECURITY_HOTSPOT) [![E](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/E-16px.png 'E')](https://sonarcloud.io/project/security_hotspots?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=SECURITY_HOTSPOT) [3 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=thelastpickle_cassandra-medusa&pullRequest=681&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=thelastpickle_cassandra-medusa&pullRequest=681) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=thelastpickle_cassandra-medusa&pullRequest=681&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=thelastpickle_cassandra-medusa&pullRequest=681&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)
rzvoncek: I'll go ahead and close this. The k8ssandra e2e test is broken, but I'm going to fix it in #686 .

Patch:
diff --git a/medusa/service/grpc/medusa.proto b/medusa/service/grpc/medusa.proto
index 8aa442d..9d4f5fc 100644
--- a/medusa/service/grpc/medusa.proto
+++ b/medusa/service/grpc/medusa.proto
@@ -44,12 +44,9 @@ message BackupStatusRequest {
 }
 
 message BackupStatusResponse {
-  repeated string  finishedNodes = 1;
-  repeated string  unfinishedNodes = 2;
-  repeated string  missingNodes = 3;
-  string           startTime = 4;
-  string           finishTime = 5;
-  StatusType status = 6;
+  string           startTime = 1;
+  string           finishTime = 2;
+  StatusType       status = 3;
 }
 
 message DeleteBackupRequest {
diff --git a/medusa/service/grpc/medusa_pb2.py b/medusa/service/grpc/medusa_pb2.py
index ee3f948..63496a5 100644
--- a/medusa/service/grpc/medusa_pb2.py
+++ b/medusa/service/grpc/medusa_pb2.py
@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
 
 
 
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cmedusa.proto\"d\n\rBackupRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12!\n\x04mode\x18\x02 \x01(\x0e\x32\x13.BackupRequest.Mode\"\"\n\x04Mode\x12\x10\n\x0c\x44IFFERENTIAL\x10\x00\x12\x08\n\x04\x46ULL\x10\x01\"A\n\x0e\x42\x61\x63kupResponse\x12\x12\n\nbackupName\x18\x01 \x01(\t\x12\x1b\n\x06status\x18\x02 \x01(\x0e\x32\x0b.StatusType\")\n\x13\x42\x61\x63kupStatusRequest\x12\x12\n\nbackupName\x18\x01 \x01(\t\"\xa0\x01\n\x14\x42\x61\x63kupStatusResponse\x12\x15\n\rfinishedNodes\x18\x01 \x03(\t\x12\x17\n\x0funfinishedNodes\x18\x02 \x03(\t\x12\x14\n\x0cmissingNodes\x18\x03 \x03(\t\x12\x11\n\tstartTime\x18\x04 \x01(\t\x12\x12\n\nfinishTime\x18\x05 \x01(\t\x12\x1b\n\x06status\x18\x06 \x01(\x0e\x32\x0b.StatusType\"#\n\x13\x44\x65leteBackupRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"A\n\x14\x44\x65leteBackupResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1b\n\x06status\x18\x02 \x01(\x0e\x32\x0b.StatusType\"&\n\x10GetBackupRequest\x12\x12\n\nbackupName\x18\x01 \x01(\t\"P\n\x11GetBackupResponse\x12\x1e\n\x06\x62\x61\x63kup\x18\x01 \x01(\x0b\x32\x0e.BackupSummary\x12\x1b\n\x06status\x18\x02 \x01(\x0e\x32\x0b.StatusType\"\x13\n\x11GetBackupsRequest\"Y\n\x12GetBackupsResponse\x12\x1f\n\x07\x62\x61\x63kups\x18\x01 \x03(\x0b\x32\x0e.BackupSummary\x12\"\n\roverallStatus\x18\x02 \x01(\x0e\x32\x0b.StatusType\"\xc2\x01\n\rBackupSummary\x12\x12\n\nbackupName\x18\x01 \x01(\t\x12\x11\n\tstartTime\x18\x02 \x01(\x03\x12\x12\n\nfinishTime\x18\x03 \x01(\x03\x12\x12\n\ntotalNodes\x18\x04 \x01(\x05\x12\x15\n\rfinishedNodes\x18\x05 \x01(\x05\x12\x1a\n\x05nodes\x18\x06 \x03(\x0b\x32\x0b.BackupNode\x12\x1b\n\x06status\x18\x07 \x01(\x0e\x32\x0b.StatusType\x12\x12\n\nbackupType\x18\x08 \x01(\t\"L\n\nBackupNode\x12\x0c\n\x04host\x18\x01 \x01(\t\x12\x0e\n\x06tokens\x18\x02 \x03(\x03\x12\x12\n\ndatacenter\x18\x03 \x01(\t\x12\x0c\n\x04rack\x18\x04 \x01(\t\"\x15\n\x13PurgeBackupsRequest\"\x84\x01\n\x14PurgeBackupsResponse\x12\x17\n\x0fnbBackupsPurged\x18\x01 \x01(\x05\x12\x17\n\x0fnbObjectsPurged\x18\x02 \x01(\x05\x12\x17\n\x0ftotalPurgedSize\x18\x03 \x01(\x03\x12!\n\x19totalObjectsWithinGcGrace\x18\x04 \x01(\x05\"S\n\x15PrepareRestoreRequest\x12\x12\n\nbackupName\x18\x01 \x01(\t\x12\x12\n\ndatacenter\x18\x02 \x01(\t\x12\x12\n\nrestoreKey\x18\x03 \x01(\t\"\x18\n\x16PrepareRestoreResponse*C\n\nStatusType\x12\x0f\n\x0bIN_PROGRESS\x10\x00\x12\x0b\n\x07SUCCESS\x10\x01\x12\n\n\x06\x46\x41ILED\x10\x02\x12\x0b\n\x07UNKNOWN\x10\x03\x32\xc8\x03\n\x06Medusa\x12)\n\x06\x42\x61\x63kup\x12\x0e.BackupRequest\x1a\x0f.BackupResponse\x12.\n\x0b\x41syncBackup\x12\x0e.BackupRequest\x1a\x0f.BackupResponse\x12;\n\x0c\x42\x61\x63kupStatus\x12\x14.BackupStatusRequest\x1a\x15.BackupStatusResponse\x12;\n\x0c\x44\x65leteBackup\x12\x14.DeleteBackupRequest\x1a\x15.DeleteBackupResponse\x12\x32\n\tGetBackup\x12\x11.GetBackupRequest\x1a\x12.GetBackupResponse\x12\x35\n\nGetBackups\x12\x12.GetBackupsRequest\x1a\x13.GetBackupsResponse\x12;\n\x0cPurgeBackups\x12\x14.PurgeBackupsRequest\x1a\x15.PurgeBackupsResponse\x12\x41\n\x0ePrepareRestore\x12\x16.PrepareRestoreRequest\x1a\x17.PrepareRestoreResponseb\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cmedusa.proto\"d\n\rBackupRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12!\n\x04mode\x18\x02 \x01(\x0e\x32\x13.BackupRequest.Mode\"\"\n\x04Mode\x12\x10\n\x0c\x44IFFERENTIAL\x10\x00\x12\x08\n\x04\x46ULL\x10\x01\"A\n\x0e\x42\x61\x63kupResponse\x12\x12\n\nbackupName\x18\x01 \x01(\t\x12\x1b\n\x06status\x18\x02 \x01(\x0e\x32\x0b.StatusType\")\n\x13\x42\x61\x63kupStatusRequest\x12\x12\n\nbackupName\x18\x01 \x01(\t\"Z\n\x14\x42\x61\x63kupStatusResponse\x12\x11\n\tstartTime\x18\x01 \x01(\t\x12\x12\n\nfinishTime\x18\x02 \x01(\t\x12\x1b\n\x06status\x18\x03 \x01(\x0e\x32\x0b.StatusType\"#\n\x13\x44\x65leteBackupRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"A\n\x14\x44\x65leteBackupResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1b\n\x06status\x18\x02 \x01(\x0e\x32\x0b.StatusType\"&\n\x10GetBackupRequest\x12\x12\n\nbackupName\x18\x01 \x01(\t\"P\n\x11GetBackupResponse\x12\x1e\n\x06\x62\x61\x63kup\x18\x01 \x01(\x0b\x32\x0e.BackupSummary\x12\x1b\n\x06status\x18\x02 \x01(\x0e\x32\x0b.StatusType\"\x13\n\x11GetBackupsRequest\"Y\n\x12GetBackupsResponse\x12\x1f\n\x07\x62\x61\x63kups\x18\x01 \x03(\x0b\x32\x0e.BackupSummary\x12\"\n\roverallStatus\x18\x02 \x01(\x0e\x32\x0b.StatusType\"\xc2\x01\n\rBackupSummary\x12\x12\n\nbackupName\x18\x01 \x01(\t\x12\x11\n\tstartTime\x18\x02 \x01(\x03\x12\x12\n\nfinishTime\x18\x03 \x01(\x03\x12\x12\n\ntotalNodes\x18\x04 \x01(\x05\x12\x15\n\rfinishedNodes\x18\x05 \x01(\x05\x12\x1a\n\x05nodes\x18\x06 \x03(\x0b\x32\x0b.BackupNode\x12\x1b\n\x06status\x18\x07 \x01(\x0e\x32\x0b.StatusType\x12\x12\n\nbackupType\x18\x08 \x01(\t\"L\n\nBackupNode\x12\x0c\n\x04host\x18\x01 \x01(\t\x12\x0e\n\x06tokens\x18\x02 \x03(\x03\x12\x12\n\ndatacenter\x18\x03 \x01(\t\x12\x0c\n\x04rack\x18\x04 \x01(\t\"\x15\n\x13PurgeBackupsRequest\"\x84\x01\n\x14PurgeBackupsResponse\x12\x17\n\x0fnbBackupsPurged\x18\x01 \x01(\x05\x12\x17\n\x0fnbObjectsPurged\x18\x02 \x01(\x05\x12\x17\n\x0ftotalPurgedSize\x18\x03 \x01(\x03\x12!\n\x19totalObjectsWithinGcGrace\x18\x04 \x01(\x05\"S\n\x15PrepareRestoreRequest\x12\x12\n\nbackupName\x18\x01 \x01(\t\x12\x12\n\ndatacenter\x18\x02 \x01(\t\x12\x12\n\nrestoreKey\x18\x03 \x01(\t\"\x18\n\x16PrepareRestoreResponse*C\n\nStatusType\x12\x0f\n\x0bIN_PROGRESS\x10\x00\x12\x0b\n\x07SUCCESS\x10\x01\x12\n\n\x06\x46\x41ILED\x10\x02\x12\x0b\n\x07UNKNOWN\x10\x03\x32\xc8\x03\n\x06Medusa\x12)\n\x06\x42\x61\x63kup\x12\x0e.BackupRequest\x1a\x0f.BackupResponse\x12.\n\x0b\x41syncBackup\x12\x0e.BackupRequest\x1a\x0f.BackupResponse\x12;\n\x0c\x42\x61\x63kupStatus\x12\x14.BackupStatusRequest\x1a\x15.BackupStatusResponse\x12;\n\x0c\x44\x65leteBackup\x12\x14.DeleteBackupRequest\x1a\x15.DeleteBackupResponse\x12\x32\n\tGetBackup\x12\x11.GetBackupRequest\x1a\x12.GetBackupResponse\x12\x35\n\nGetBackups\x12\x12.GetBackupsRequest\x1a\x13.GetBackupsResponse\x12;\n\x0cPurgeBackups\x12\x14.PurgeBackupsRequest\x1a\x15.PurgeBackupsResponse\x12\x41\n\x0ePrepareRestore\x12\x16.PrepareRestoreRequest\x1a\x17.PrepareRestoreResponseb\x06proto3')
 
 _globals = globals()
 _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -21,8 +21,8 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'medusa_pb2', _globals)
 if _descriptor._USE_C_DESCRIPTORS == False:
 
   DESCRIPTOR._options = None
-  _globals['_STATUSTYPE']._serialized_start=1273
-  _globals['_STATUSTYPE']._serialized_end=1340
+  _globals['_STATUSTYPE']._serialized_start=1202
+  _globals['_STATUSTYPE']._serialized_end=1269
   _globals['_BACKUPREQUEST']._serialized_start=16
   _globals['_BACKUPREQUEST']._serialized_end=116
   _globals['_BACKUPREQUEST_MODE']._serialized_start=82
@@ -31,32 +31,32 @@ if _descriptor._USE_C_DESCRIPTORS == False:
   _globals['_BACKUPRESPONSE']._serialized_end=183
   _globals['_BACKUPSTATUSREQUEST']._serialized_start=185
   _globals['_BACKUPSTATUSREQUEST']._serialized_end=226
-  _globals['_BACKUPSTATUSRESPONSE']._serialized_start=229
-  _globals['_BACKUPSTATUSRESPONSE']._serialized_end=389
-  _globals['_DELETEBACKUPREQUEST']._serialized_start=391
-  _globals['_DELETEBACKUPREQUEST']._serialized_end=426
-  _globals['_DELETEBACKUPRESPONSE']._serialized_start=428
-  _globals['_DELETEBACKUPRESPONSE']._serialized_end=493
-  _globals['_GETBACKUPREQUEST']._serialized_start=495
-  _globals['_GETBACKUPREQUEST']._serialized_end=533
-  _globals['_GETBACKUPRESPONSE']._serialized_start=535
-  _globals['_GETBACKUPRESPONSE']._serialized_end=615
-  _globals['_GETBACKUPSREQUEST']._serialized_start=617
-  _globals['_GETBACKUPSREQUEST']._serialized_end=636
-  _globals['_GETBACKUPSRESPONSE']._serialized_start=638
-  _globals['_GETBACKUPSRESPONSE']._serialized_end=727
-  _globals['_BACKUPSUMMARY']._serialized_start=730
-  _globals['_BACKUPSUMMARY']._serialized_end=924
-  _globals['_BACKUPNODE']._serialized_start=926
-  _globals['_BACKUPNODE']._serialized_end=1002
-  _globals['_PURGEBACKUPSREQUEST']._serialized_start=1004
-  _globals['_PURGEBACKUPSREQUEST']._serialized_end=1025
-  _globals['_PURGEBACKUPSRESPONSE']._serialized_start=1028
-  _globals['_PURGEBACKUPSRESPONSE']._serialized_end=1160
-  _globals['_PREPARERESTOREREQUEST']._serialized_start=1162
-  _globals['_PREPARERESTOREREQUEST']._serialized_end=1245
-  _globals['_PREPARERESTORERESPONSE']._serialized_start=1247
-  _globals['_PREPARERESTORERESPONSE']._serialized_end=1271
-  _globals['_MEDUSA']._serialized_start=1343
-  _globals['_MEDUSA']._serialized_end=1799
+  _globals['_BACKUPSTATUSRESPONSE']._serialized_start=228
+  _globals['_BACKUPSTATUSRESPONSE']._serialized_end=318
+  _globals['_DELETEBACKUPREQUEST']._serialized_start=320
+  _globals['_DELETEBACKUPREQUEST']._serialized_end=355
+  _globals['_DELETEBACKUPRESPONSE']._serialized_start=357
+  _globals['_DELETEBACKUPRESPONSE']._serialized_end=422
+  _globals['_GETBACKUPREQUEST']._serialized_start=424
+  _globals['_GETBACKUPREQUEST']._serialized_end=462
+  _globals['_GETBACKUPRESPONSE']._serialized_start=464
+  _globals['_GETBACKUPRESPONSE']._serialized_end=544
+  _globals['_GETBACKUPSREQUEST']._serialized_start=546
+  _globals['_GETBACKUPSREQUEST']._serialized_end=565
+  _globals['_GETBACKUPSRESPONSE']._serialized_start=567
+  _globals['_GETBACKUPSRESPONSE']._serialized_end=656
+  _globals['_BACKUPSUMMARY']._serialized_start=659
+  _globals['_BACKUPSUMMARY']._serialized_end=853
+  _globals['_BACKUPNODE']._serialized_start=855
+  _globals['_BACKUPNODE']._serialized_end=931
+  _globals['_PURGEBACKUPSREQUEST']._serialized_start=933
+  _globals['_PURGEBACKUPSREQUEST']._serialized_end=954
+  _globals['_PURGEBACKUPSRESPONSE']._serialized_start=957
+  _globals['_PURGEBACKUPSRESPONSE']._serialized_end=1089
+  _globals['_PREPARERESTOREREQUEST']._serialized_start=1091
+  _globals['_PREPARERESTOREREQUEST']._serialized_end=1174
+  _globals['_PREPARERESTORERESPONSE']._serialized_start=1176
+  _globals['_PREPARERESTORERESPONSE']._serialized_end=1200
+  _globals['_MEDUSA']._serialized_start=1272
+  _globals['_MEDUSA']._serialized_end=1728
 # @@protoc_insertion_point(module_scope)
diff --git a/medusa/service/grpc/server.py b/medusa/service/grpc/server.py
index 17b872a..f2f2fcb 100644
--- a/medusa/service/grpc/server.py
+++ b/medusa/service/grpc/server.py
@@ -167,28 +167,27 @@ class MedusaService(medusa_pb2_grpc.MedusaServicer):
         return response
 
     def BackupStatus(self, request, context):
-
         response = medusa_pb2.BackupStatusResponse()
         try:
             with Storage(config=self.storage_config) as storage:
-                backup = storage.get_cluster_backup(request.backupName)
 
-                # TODO how is the startTime determined?
-                response.startTime = datetime.fromtimestamp(backup.started).strftime(TIMESTAMP_FORMAT)
-                response.finishedNodes.extend([node.fqdn for node in backup.complete_nodes()])
-                response.unfinishedNodes.extend([node.fqdn for node in backup.incomplete_nodes()])
-                response.missingNodes.extend(backup.missing_nodes())
+                backup = storage.get_node_backup(fqdn=storage.config.fqdn, name=request.backupName)
+                if backup.started is None:
+                    raise KeyError
 
+                response.startTime = datetime.fromtimestamp(backup.started).strftime(TIMESTAMP_FORMAT)
                 if backup.finished:
                     response.finishTime = datetime.fromtimestamp(backup.finished).strftime(TIMESTAMP_FORMAT)
                 else:
                     response.finishTime = ""
 
                 record_status_in_response(response, request.backupName)
+
         except KeyError:
             context.set_details("backup <{}> does not exist".format(request.backupName))
             context.set_code(grpc.StatusCode.NOT_FOUND)
             response.status = medusa_pb2.StatusType.UNKNOWN
+
         return response
 
     def GetBackup(self, request, context):
diff --git a/tox.ini b/tox.ini
index ee90330..bc6126e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,6 +11,9 @@ commands =
     flake8 . --ignore=W503,E402 --exclude=medusa/service/grpc/medusa_pb2.py,medusa/service/grpc/medusa_pb2_grpc.py,.tox,venv,build,dist,debian
     pytest --cov=medusa --cov-report=xml -v {posargs:tests/}
 
+setenv =
+    DYLD_LIBRARY_PATH = {env:DYLD_LIBRARY_PATH}
+
 [flake8]
 exclude = .tox,*.egg,build,data,scripts,env,venv
 select = E,W,F


Test patch:
diff --git a/tests/purge_test.py b/tests/purge_test.py
index 8ffe979..545dac5 100644
--- a/tests/purge_test.py
+++ b/tests/purge_test.py
@@ -14,19 +14,17 @@
 # limitations under the License.
 
 import configparser
-import hashlib
-import os
 import unittest
 
 from datetime import datetime, timedelta
-from random import randrange
 
 from medusa.config import MedusaConfig, StorageConfig, _namedtuple_from_dict
-from medusa.storage import NodeBackup, Storage, ClusterBackup
-from medusa.storage.abstract_storage import AbstractBlob
+from medusa.storage import Storage
 from medusa.purge import backups_to_purge_by_age, backups_to_purge_by_count, backups_to_purge_by_name
 from medusa.purge import filter_differential_backups, filter_files_within_gc_grace
 
+from tests.storage_test import make_node_backup, make_cluster_backup, make_blob
+
 
 class PurgeTest(unittest.TestCase):
     def __init__(self, *args, **kwargs):
@@ -59,7 +57,7 @@ class PurgeTest(unittest.TestCase):
         # Build a list of 40 daily backups
         for i in range(1, 80, 2):
             file_time = datetime.now() + timedelta(days=(i + 1) - 80)
-            backups.append(self.make_node_backup(self.storage, str(i), file_time, differential=True))
+            backups.append(make_node_backup(self.storage, str(i), file_time, differential=True))
 
         obsolete_backups = backups_to_purge_by_age(backups, 1)
         assert len(obsolete_backups) == 39
@@ -82,7 +80,7 @@ class PurgeTest(unittest.TestCase):
         # Build a list of 40 daily backups
         for i in range(1, 80, 2):
             file_time = datetime.now() + timedelta(days=(i + 1) - 80)
-            backups.append(self.make_node_backup(self.storage, str(i), file_time, differential=True))
+            backups.append(make_node_backup(self.storage, str(i), file_time, differential=True))
 
         obsolete_backups = backups_to_purge_by_count(backups, 10)
         assert(obsolete_backups[0].name == "1")
@@ -95,19 +93,19 @@ class PurgeTest(unittest.TestCase):
 
     def test_filter_differential_backups(self):
         backups = list()
-        backups.append(self.make_node_backup(self.storage, "one", datetime.now(), differential=True))
-        backups.append(self.make_node_backup(self.storage, "two", datetime.now(), differential=True))
-        backups.append(self.make_node_backup(self.storage, "three", datetime.now(), differential=False))
-        backups.append(self.make_node_backup(self.storage, "four", datetime.now(), differential=True))
-        backups.append(self.make_node_backup(self.storage, "five", datetime.now(), differential=False))
+        backups.append(make_node_backup(self.storage, "one", datetime.now(), differential=True))
+        backups.append(make_node_backup(self.storage, "two", datetime.now(), differential=True))
+        backups.append(make_node_backup(self.storage, "three", datetime.now(), differential=False))
+        backups.append(make_node_backup(self.storage, "four", datetime.now(), differential=True))
+        backups.append(make_node_backup(self.storage, "five", datetime.now(), differential=False))
         assert 3 == len(filter_differential_backups(backups))
 
     def test_filter_files_within_gc_grace(self):
         blobs = list()
-        blobs.append(self.make_blob("file1", datetime.timestamp(datetime.now())))
-        blobs.append(self.make_blob("file2", datetime.timestamp(datetime.now() + timedelta(hours=-12))))
-        blobs.append(self.make_blob("file3", datetime.timestamp(datetime.now() + timedelta(days=-1))))
-        blobs.append(self.make_blob("file4", datetime.timestamp(datetime.now() + timedelta(days=-2))))
+        blobs.append(make_blob("file1", datetime.timestamp(datetime.now())))
+        blobs.append(make_blob("file2", datetime.timestamp(datetime.now() + timedelta(hours=-12))))
+        blobs.append(make_blob("file3", datetime.timestamp(datetime.now() + timedelta(days=-1))))
+        blobs.append(make_blob("file4", datetime.timestamp(datetime.now() + timedelta(days=-2))))
         blob_map = {blob.name: blob for blob in blobs}
         assert 2 == len(filter_files_within_gc_grace(self.storage, blob_map.keys(), blob_map, 1))
 
@@ -116,8 +114,9 @@ class PurgeTest(unittest.TestCase):
         backup_names = ["backup1", "backup2", "backup3"]
         cluster_backups = list()
         for backup_name in backup_names:
-            cluster_backups.append(self.make_cluster_backup(self.storage, backup_name, datetime.now(), nodes,
-                                                            differential=True))
+            cluster_backups.append(
+                make_cluster_backup(self.storage, backup_name, datetime.now(), nodes, differential=True)
+            )
 
         # all nodes, one backups
         backups_to_purge = backups_to_purge_by_name(self.storage, cluster_backups, ["backup1"], True)
@@ -150,30 +149,6 @@ class PurgeTest(unittest.TestCase):
         # non-existent backup name raises KeyError
         self.assertRaises(KeyError, backups_to_purge_by_name, self.storage, cluster_backups, ["nonexistent"], False)
 
-    def make_node_backup(self, storage, name, backup_date, differential=False, fqdn="localhost"):
-        if differential is True:
-            differential_blob = self.make_blob("localhost/{}/meta/differential".format(name), backup_date.timestamp())
-        else:
-            differential_blob = None
-        tokenmap_blob = self.make_blob("localhost/{}/meta/tokenmap.json".format(name), backup_date.timestamp())
-        schema_blob = self.make_blob("localhost/{}/meta/schema.cql".format(name), backup_date.timestamp())
-        manifest_blob = self.make_blob("localhost/{}/meta/manifest.json".format(name), backup_date.timestamp())
-        return NodeBackup(storage=storage, fqdn=fqdn, name=str(name),
-                          differential_blob=differential_blob, manifest_blob=manifest_blob,
-                          tokenmap_blob=tokenmap_blob, schema_blob=schema_blob,
-                          started_timestamp=backup_date.timestamp(), finished_timestamp=backup_date.timestamp())
-
-    def make_cluster_backup(self, storage, name, backup_date, nodes, differential=False):
-        node_backups = list()
-        for node in nodes:
-            node_backups.append(self.make_node_backup(storage, name, backup_date, differential, node))
-        return ClusterBackup(name, node_backups)
-
-    def make_blob(self, blob_name, blob_date):
-        checksum = hashlib.md5()
-        checksum.update(os.urandom(4))
-        return AbstractBlob(blob_name, randrange(100), checksum.hexdigest(), datetime.fromtimestamp(blob_date))
-
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/server_test.py b/tests/service/grpc/server_test.py
similarity index 62%
rename from tests/server_test.py
rename to tests/service/grpc/server_test.py
index 8e69a9e..723354e 100644
--- a/tests/server_test.py
+++ b/tests/service/grpc/server_test.py
@@ -24,16 +24,12 @@ from medusa.service.grpc import medusa_pb2
 from medusa.service.grpc.server import MedusaService
 from medusa.storage import Storage
 
-# goes away after we merge #681 and rebase on top of it
-from tests.purge_test import PurgeTest
+from tests.storage_test import make_node_backup, make_cluster_backup
 
 
 class ServerTest(unittest.TestCase):
 
     def __init__(self, *args, **kwargs):
-        # goes away after we merge #681 and rebase on top of it
-        self.make_node_backup = PurgeTest().make_node_backup
-        self.make_cluster_backup = PurgeTest().make_cluster_backup
         super().__init__(*args, **kwargs)
 
     def _make_config(self):
@@ -98,7 +94,7 @@ class ServerTest(unittest.TestCase):
 
         # build a fake cluster backup object
         storage = Storage(config=medusa_config.storage)
-        cluster_backup = self.make_cluster_backup(
+        cluster_backup = make_cluster_backup(
             storage=storage,
             name='backup1',
             backup_date=datetime.fromtimestamp(123456),
@@ -143,6 +139,80 @@ class ServerTest(unittest.TestCase):
                     self.assertEqual(medusa_pb2.StatusType.SUCCESS, get_backup_response.backup.status)
                     self.assertEqual('differential', get_backup_response.backup.backupType)
 
+    def test_get_backup_status_unknown_backup(self):
+        # start the Medusa service
+        medusa_config = self._make_config()
+        service = MedusaService(medusa_config)
+
+        # make a status request for an unknown backup.
+        # no need to fake a backup here because we're not actually interested in it
+        request = medusa_pb2.BackupStatusRequest(backupName='unknown_backup')
+        context = Mock(spec=ServicerContext)
+        backup_status = service.BackupStatus(request, context)
+
+        # verify the response structure without an emphasis on the actual contents
+        status_fields = {field.name for field in backup_status.DESCRIPTOR.fields}
+        self.assertEqual(
+            status_fields,
+            {'startTime', 'finishTime', 'status'}
+        )
+        self.assertEqual(medusa_pb2.StatusType.UNKNOWN, backup_status.status)
+        self.assertEqual('', backup_status.startTime)
+        self.assertEqual('', backup_status.finishTime)
+
+    def test_get_backup_status_known_backup(self):
+        # start the Medusa service
+        medusa_config = self._make_config()
+        service = MedusaService(medusa_config)
+
+        # build a fake backup object
+        storage = Storage(config=medusa_config.storage)
+        node_backup = make_node_backup(storage, 'backup1', datetime.fromtimestamp(123456), differential=True)
+        BackupMan.register_backup('backup1', True)
+        BackupMan.update_backup_status('backup1', BackupMan.STATUS_SUCCESS)
+
+        # this patch prevents the actual get_node_backup call, and instead returns the fake backup from above
+        with patch('medusa.storage.Storage.get_node_backup', return_value=node_backup):
+            request = medusa_pb2.BackupStatusRequest(backupName='backup1')
+            context = Mock(spec=ServicerContext)
+            backup_status = service.BackupStatus(request, context)
+
+            self.assertEqual(medusa_pb2.StatusType.SUCCESS, backup_status.status)
+
+            # the start timestamp of 123456 rendered as a human-readable date
+            # we're parsing this back and forth because hard-coding a human-readable date is
+            # inconsistent across environments
+            start_time = int(datetime.strptime(backup_status.startTime, '%Y-%m-%d %H:%M:%S').timestamp())
+            self.assertEqual(123456, start_time)
+            # the fake backup is instant - starts and finishes at the same time
+            finish_time = int(datetime.strptime(backup_status.finishTime, '%Y-%m-%d %H:%M:%S').timestamp())
+            self.assertEqual(123456, finish_time)
+
+    def test_get_backup_status_incomplete_backup(self):
+        # start the Medusa service
+        medusa_config = self._make_config()
+        service = MedusaService(medusa_config)
+
+        # build a fake backup object
+        storage = Storage(config=medusa_config.storage)
+        node_backup = make_node_backup(storage, 'backup2', datetime.fromtimestamp(123456), differential=True)
+        # we only register the backup, do not move it to SUCCESS
+        BackupMan.register_backup('backup2', True)
+
+        with patch('medusa.storage.Storage.get_node_backup', return_value=node_backup):
+            request = medusa_pb2.BackupStatusRequest(backupName='backup2')
+            context = Mock(spec=ServicerContext)
+            backup_status = service.BackupStatus(request, context)
+
+            # we get the response as IN_PROGRESS
+            self.assertEqual(medusa_pb2.StatusType.IN_PROGRESS, backup_status.status)
+            # the finish time is already set
+            # I'm not sure this is because of faking the backup
+            start_time = int(datetime.strptime(backup_status.startTime, '%Y-%m-%d %H:%M:%S').timestamp())
+            self.assertEqual(123456, start_time)
+            finish_time = int(datetime.strptime(backup_status.finishTime, '%Y-%m-%d %H:%M:%S').timestamp())
+            self.assertEqual(123456, finish_time)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/storage_test.py b/tests/storage_test.py
index 42146f2..78f3dfd 100644
--- a/tests/storage_test.py
+++ b/tests/storage_test.py
@@ -21,15 +21,19 @@ import shutil
 import tempfile
 import unittest
 
+from datetime import datetime
+from random import randrange
+
 import medusa.storage.abstract_storage
 
-from medusa.storage.abstract_storage import AbstractStorage
+from medusa.storage import NodeBackup, ClusterBackup
+from medusa.storage.abstract_storage import AbstractStorage, AbstractBlob
 from medusa.config import MedusaConfig, StorageConfig, _namedtuple_from_dict, CassandraConfig
 from medusa.index import build_indices
 from medusa.storage import Storage
 
 
-class RestoreNodeTest(unittest.TestCase):
+class StorageTest(unittest.TestCase):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.local_storage_dir = "/tmp/medusa_local_storage"
@@ -352,5 +356,32 @@ class RestoreNodeTest(unittest.TestCase):
         )
 
 
+def make_node_backup(storage, name, backup_date, differential=False, fqdn="localhost"):
+    if differential is True:
+        differential_blob = make_blob("localhost/{}/meta/differential".format(name), backup_date.timestamp())
+    else:
+        differential_blob = None
+    tokenmap_blob = make_blob("localhost/{}/meta/tokenmap.json".format(name), backup_date.timestamp())
+    schema_blob = make_blob("localhost/{}/meta/schema.cql".format(name), backup_date.timestamp())
+    manifest_blob = make_blob("localhost/{}/meta/manifest.json".format(name), backup_date.timestamp())
+    return NodeBackup(storage=storage, fqdn=fqdn, name=str(name),
+                      differential_blob=differential_blob, manifest_blob=manifest_blob,
+                      tokenmap_blob=tokenmap_blob, schema_blob=schema_blob,
+                      started_timestamp=backup_date.timestamp(), finished_timestamp=backup_date.timestamp())
+
+
+def make_cluster_backup(storage, name, backup_date, nodes, differential=False):
+    node_backups = list()
+    for node in nodes:
+        node_backups.append(make_node_backup(storage, name, backup_date, differential, node))
+    return ClusterBackup(name, node_backups)
+
+
+def make_blob(blob_name, blob_date):
+    checksum = hashlib.md5()
+    checksum.update(os.urandom(4))
+    return AbstractBlob(blob_name, randrange(100), checksum.hexdigest(), datetime.fromtimestamp(blob_date))
+
+
 if __name__ == '__main__':
     unittest.main()