diff --git a/meilisearch/src/error.rs b/meilisearch/src/error.rs index 9c77f4d3e..fcfe4b7fc 100644 --- a/meilisearch/src/error.rs +++ b/meilisearch/src/error.rs @@ -24,8 +24,8 @@ pub enum MeilisearchHttpError { MissingPayload(PayloadType), #[error("The provided payload reached the size limit.")] PayloadTooLarge, - #[error("Two indexes must be given for each swap. The list `{:?}` contains {} indexes.", - .0, .0.len() + #[error("Two indexes must be given for each swap. The list `[{}]` contains {} indexes.", + .0.iter().map(|uid| format!("\"{uid}\"")).collect::>().join(", "), .0.len() )] SwapIndexPayloadWrongLength(Vec), #[error(transparent)] diff --git a/meilisearch/tests/integration.rs b/meilisearch/tests/integration.rs index 25b4e49b6..4383aea57 100644 --- a/meilisearch/tests/integration.rs +++ b/meilisearch/tests/integration.rs @@ -8,6 +8,7 @@ mod search; mod settings; mod snapshot; mod stats; +mod swap_indexes; mod tasks; // Tests are isolated by features in different modules to allow better readability, test diff --git a/meilisearch/tests/swap_indexes/errors.rs b/meilisearch/tests/swap_indexes/errors.rs new file mode 100644 index 000000000..848f347f8 --- /dev/null +++ b/meilisearch/tests/swap_indexes/errors.rs @@ -0,0 +1,94 @@ +use meili_snap::*; +use serde_json::json; + +use crate::common::Server; + +#[actix_rt::test] +async fn swap_indexes_bad_format() { + let server = Server::new().await; + + let (response, code) = server.index_swap(json!("doggo")).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad-request" + } + "###); + + let (response, code) = server.index_swap(json!(["doggo"])).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value type at `[0]`: expected an object, but found a string: `\"doggo\"`", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad-request" + } + "###); +} + +#[actix_rt::test] +async fn swap_indexes_bad_indexes() { + let server = Server::new().await; + + let (response, code) = server.index_swap(json!([{ "indexes": "doggo"}])).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value type at `[0].indexes`: expected an array, but found a string: `\"doggo\"`", + "code": "invalid_swap_indexes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-swap-indexes" + } + "###); + + let (response, code) = server.index_swap(json!([{ "indexes": ["doggo"]}])).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Two indexes must be given for each swap. The list `[\"doggo\"]` contains 1 indexes.", + "code": "invalid_swap_indexes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-swap-indexes" + } + "###); + + let (response, code) = + server.index_swap(json!([{ "indexes": ["doggo", "crabo", "croco"]}])).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Two indexes must be given for each swap. The list `[\"doggo\", \"crabo\", \"croco\"]` contains 3 indexes.", + "code": "invalid_swap_indexes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-swap-indexes" + } + "###); + + let (response, code) = server.index_swap(json!([{ "indexes": ["doggo", "doggo"]}])).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Indexes must be declared only once during a swap. `doggo` was specified several times.", + "code": "invalid_swap_duplicate_index_found", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-swap-duplicate-index-found" + } + "###); + + let (response, code) = server + .index_swap(json!([{ "indexes": ["doggo", "catto"]}, { "indexes": ["girafo", "doggo"]}])) + .await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Indexes must be declared only once during a swap. `doggo` was specified several times.", + "code": "invalid_swap_duplicate_index_found", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-swap-duplicate-index-found" + } + "###); +} diff --git a/meilisearch/tests/swap_indexes/mod.rs b/meilisearch/tests/swap_indexes/mod.rs new file mode 100644 index 000000000..42d92e2af --- /dev/null +++ b/meilisearch/tests/swap_indexes/mod.rs @@ -0,0 +1,357 @@ +mod errors; + +use meili_snap::{json_string, snapshot}; +use serde_json::json; + +use crate::common::{GetAllDocumentsOptions, Server}; + +#[actix_rt::test] +async fn swap_indexes() { + let server = Server::new().await; + let a = server.index("a"); + let (_, code) = a.add_documents(json!({ "id": 1, "index": "a"}), None).await; + snapshot!(code, @"202 Accepted"); + let b = server.index("b"); + let (res, code) = b.add_documents(json!({ "id": 1, "index": "b"}), None).await; + snapshot!(code, @"202 Accepted"); + snapshot!(res["taskUid"], @"1"); + server.wait_task(1).await; + + let (tasks, code) = server.tasks().await; + snapshot!(code, @"200 OK"); + snapshot!(json_string!(tasks, { ".results[].duration" => "[duration]", ".results[].enqueuedAt" => "[date]", ".results[].startedAt" => "[date]", ".results[].finishedAt" => "[date]" }), @r###" + { + "results": [ + { + "uid": 1, + "indexUid": "b", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 0, + "indexUid": "a", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + ], + "limit": 20, + "from": 1, + "next": null + } + "###); + + let (res, code) = server.index_swap(json!([{ "indexes": ["a", "b"] }])).await; + snapshot!(code, @"202 Accepted"); + snapshot!(res["taskUid"], @"2"); + server.wait_task(2).await; + + let (tasks, code) = server.tasks().await; + snapshot!(code, @"200 OK"); + + // Notice how the task 0 which was initially representing the creation of the index `A` now represents the creation of the index `B`. + snapshot!(json_string!(tasks, { ".results[].duration" => "[duration]", ".results[].enqueuedAt" => "[date]", ".results[].startedAt" => "[date]", ".results[].finishedAt" => "[date]" }), @r###" + { + "results": [ + { + "uid": 2, + "indexUid": null, + "status": "succeeded", + "type": "indexSwap", + "canceledBy": null, + "details": { + "swaps": [ + { + "indexes": [ + "a", + "b" + ] + } + ] + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 1, + "indexUid": "a", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 0, + "indexUid": "b", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + ], + "limit": 20, + "from": 2, + "next": null + } + "###); + + // BUT, the data in `a` should now points to the data that was in `b`. + // And the opposite is true as well + let (res, _) = a.get_all_documents(GetAllDocumentsOptions::default()).await; + snapshot!(res["results"], @r###"[{"id":1,"index":"b"}]"###); + let (res, _) = b.get_all_documents(GetAllDocumentsOptions::default()).await; + snapshot!(res["results"], @r###"[{"id":1,"index":"a"}]"###); + + // ================ + // And now we're going to attempt the famous and dangerous DOUBLE index swap 🤞 + + let c = server.index("c"); + let (res, code) = c.add_documents(json!({ "id": 1, "index": "c"}), None).await; + snapshot!(code, @"202 Accepted"); + snapshot!(res["taskUid"], @"3"); + let d = server.index("d"); + let (res, code) = d.add_documents(json!({ "id": 1, "index": "d"}), None).await; + snapshot!(code, @"202 Accepted"); + snapshot!(res["taskUid"], @"4"); + server.wait_task(4).await; + + // ensure the index creation worked properly + let (tasks, code) = server.tasks_filter("limit=2").await; + snapshot!(code, @"200 OK"); + snapshot!(json_string!(tasks, { ".results[].duration" => "[duration]", ".results[].enqueuedAt" => "[date]", ".results[].startedAt" => "[date]", ".results[].finishedAt" => "[date]" }), @r###" + { + "results": [ + { + "uid": 4, + "indexUid": "d", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 3, + "indexUid": "c", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + ], + "limit": 2, + "from": 4, + "next": 2 + } + "###); + + // It's happening 😲 + + let (res, code) = + server.index_swap(json!([{ "indexes": ["a", "b"] }, { "indexes": ["c", "d"] } ])).await; + snapshot!(res["taskUid"], @"5"); + snapshot!(code, @"202 Accepted"); + server.wait_task(5).await; + + // ensure the index creation worked properly + let (tasks, code) = server.tasks().await; + snapshot!(code, @"200 OK"); + + // What should we check for each tasks in this test: + // Task number; + // 0. should have the indexUid `a` again + // 1. should have the indexUid `b` again + // 2. stays unchanged + // 3. now have the indexUid `d` instead of `c` + // 4. now have the indexUid `c` instead of `d` + snapshot!(json_string!(tasks, { ".results[].duration" => "[duration]", ".results[].enqueuedAt" => "[date]", ".results[].startedAt" => "[date]", ".results[].finishedAt" => "[date]" }), @r###" + { + "results": [ + { + "uid": 5, + "indexUid": null, + "status": "succeeded", + "type": "indexSwap", + "canceledBy": null, + "details": { + "swaps": [ + { + "indexes": [ + "a", + "b" + ] + }, + { + "indexes": [ + "c", + "d" + ] + } + ] + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 4, + "indexUid": "c", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 3, + "indexUid": "d", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 2, + "indexUid": null, + "status": "succeeded", + "type": "indexSwap", + "canceledBy": null, + "details": { + "swaps": [ + { + "indexes": [ + "b", + "a" + ] + } + ] + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 1, + "indexUid": "b", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 0, + "indexUid": "a", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + ], + "limit": 20, + "from": 5, + "next": null + } + "###); + + // - The data in `a` should point to `a`. + // - The data in `b` should point to `b`. + // - The data in `c` should point to `d`. + // - The data in `d` should point to `c`. + let (res, _) = a.get_all_documents(GetAllDocumentsOptions::default()).await; + snapshot!(res["results"], @r###"[{"id":1,"index":"a"}]"###); + let (res, _) = b.get_all_documents(GetAllDocumentsOptions::default()).await; + snapshot!(res["results"], @r###"[{"id":1,"index":"b"}]"###); + let (res, _) = c.get_all_documents(GetAllDocumentsOptions::default()).await; + snapshot!(res["results"], @r###"[{"id":1,"index":"d"}]"###); + let (res, _) = d.get_all_documents(GetAllDocumentsOptions::default()).await; + snapshot!(res["results"], @r###"[{"id":1,"index":"c"}]"###); +}