max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+47 insertions,
-8 deletions
| @@ -1,10 +1,11 @@ | |||
| 1 | 1 | //! OTA update integration tests, slug management, releases, artifacts, updater endpoint. | |
| 2 | 2 | ||
| 3 | - | use crate::harness::TestHarness; | |
| 3 | + | use crate::harness::{BuildOptions, TestHarness, storage::InMemoryStorage}; | |
| 4 | 4 | use makenotwork::db::{OtaReleaseId, SyncAppId, UserId}; | |
| 5 | 5 | use serde::Deserialize; | |
| 6 | 6 | use serde_json::json; | |
| 7 | 7 | use sqlx::PgPool; | |
| 8 | + | use std::sync::Arc; | |
| 8 | 9 | ||
| 9 | 10 | /// The artifact-register route rejects signatures shorter than 40 chars (a real | |
| 10 | 11 | /// base64-encoded minisign signature is well over that), so any test that | |
| @@ -38,6 +39,7 @@ | |||
| 38 | 39 | #[derive(Deserialize)] | |
| 39 | 40 | struct UploadArtifactResponse { | |
| 40 | 41 | upload_url: String, | |
| 42 | + | s3_key: String, | |
| 41 | 43 | } | |
| 42 | 44 | ||
| 43 | 45 | #[derive(Deserialize)] | |
| @@ -123,6 +125,19 @@ | |||
| 123 | 125 | TestHarness::with_synckit_storage().await | |
| 124 | 126 | } | |
| 125 | 127 | ||
| 128 | + | /// Same harness, with the storage handle kept. The register-artifact route only | |
| 129 | + | /// presigns, so nothing ever lands in the backend; a download test has to put | |
| 130 | + | /// the object there itself, standing in for the client PUT. | |
| 131 | + | async fn harness_with_synckit_blobs() -> (TestHarness, Arc<InMemoryStorage>) { | |
| 132 | + | let mem = Arc::new(InMemoryStorage::new()); | |
| 133 | + | let h = TestHarness::build(BuildOptions { | |
| 134 | + | synckit_storage: Some(mem.clone()), | |
| 135 | + | ..Default::default() | |
| 136 | + | }) | |
| 137 | + | .await; | |
| 138 | + | (h, mem) | |
| 139 | + | } | |
| 140 | + | ||
| 126 | 141 | /// Mark a release's artifacts scanned-clean (simulates a completed scan). New | |
| 127 | 142 | /// artifacts start `pending` and are not advertised/downloadable until the scan | |
| 128 | 143 | /// pipeline clears them; serve-path tests call this to reach the served state. | |
| @@ -812,7 +827,7 @@ | |||
| 812 | 827 | ||
| 813 | 828 | #[tokio::test] | |
| 814 | 829 | async fn artifact_download_redirect() { | |
| 815 | - | let mut h = harness_with_synckit_storage().await; | |
| 830 | + | let (mut h, blobs) = harness_with_synckit_blobs().await; | |
| 816 | 831 | let user_id = h.signup("otauser", "ota@example.com", "Password1!").await; | |
| 817 | 832 | let (app_id, api_key) = create_sync_app_with_slug(&h.db, user_id, "dlapp").await; | |
| 818 | 833 | ||
| @@ -853,14 +868,38 @@ | |||
| 853 | 868 | ) | |
| 854 | 869 | .await; | |
| 855 | 870 | assert_eq!(resp.status, 201); | |
| 871 | + | let upload: UploadArtifactResponse = resp.json(); | |
| 856 | 872 | ||
| 857 | - | // The s3_key the download route would presign. | |
| 858 | - | let _s3_key = format!("ota/{app_id}/1.0.0/linux/x86_64/artifact"); | |
| 873 | + | // The client PUT the route presigned for, performed against the backend | |
| 874 | + | // directly: `presign_download` refuses a key with no object behind it, and | |
| 875 | + | // the download route checks `object_exists` before it presigns anything. | |
| 876 | + | blobs.put(&upload.s3_key, b"artifact bytes".to_vec()); | |
| 877 | + | mark_artifacts_clean(&h.db, &release.id).await; | |
| 859 | 878 | ||
| 860 | - | // The redirect itself is not exercised: `InMemoryStorage::presign_download` | |
| 861 | - | // checks the object exists, the upload endpoint only presigns, and the | |
| 862 | - | // harness does not expose `synckit_storage` for a manual injection. This | |
| 863 | - | // test covers release and artifact creation only. | |
| 879 | + | let resp = h | |
| 880 | + | .client | |
| 881 | + | .get(&format!( | |
| 882 | + | "/api/sync/ota/dlapp/download/{}/linux/x86_64", | |
| 883 | + | release.id | |
| 884 | + | )) | |
| 885 | + | .await; | |
| 886 | + | assert_eq!(resp.status, 302, "download: {}", resp.text); | |
| 887 | + | let expected = format!("http://test-storage/{}", upload.s3_key); | |
| 888 | + | assert_eq!( | |
| 889 | + | resp.header("location"), | |
| 890 | + | Some(expected.as_str()), | |
| 891 | + | "redirects to a presigned URL for the artifact's own key" | |
| 892 | + | ); | |
| 893 | + | ||
| 894 | + | // A target with no artifact 404s rather than redirecting somewhere wrong. | |
| 895 | + | let resp = h | |
| 896 | + | .client | |
| 897 | + | .get(&format!( | |
| 898 | + | "/api/sync/ota/dlapp/download/{}/darwin/aarch64", | |
| 899 | + | release.id | |
| 900 | + | )) | |
| 901 | + | .await; | |
| 902 | + | assert_eq!(resp.status, 404, "no darwin artifact: {}", resp.text); | |
| 864 | 903 | } | |
| 865 | 904 | ||
| 866 | 905 | #[tokio::test] |