Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions AzureSearchEmulator.IntegrationTests/EmulatorIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,78 @@ public async Task SearchDocuments_WithPaging_ShouldReturnPagedResults()
await indexClient.DeleteIndexAsync(indexName);
}

[Fact]
public async Task CreateOrUpdateIndex_WhenIndexDoesNotExist_ShouldCreate()
{
// Arrange
const string indexName = "test-createorupdate-create";
var indexClient = factory.CreateSearchIndexClient();

// Ensure a clean slate
try
{
await indexClient.DeleteIndexAsync(indexName);
}
catch (Azure.RequestFailedException ex) when (ex.Status == 404)
{
// expected
}

var index = new SearchIndex(indexName)
{
Fields =
[
new SearchField(nameof(Product.Id), SearchFieldDataType.String) { IsKey = true, IsStored = true, IsSearchable = true, IsFilterable = true },
new SearchField(nameof(Product.Name), SearchFieldDataType.String) { IsSearchable = true, IsStored = true }
]
};

// Act - PUT to a non-existent index should create it
var result = await indexClient.CreateOrUpdateIndexAsync(index);

// Assert
Assert.NotNull(result);
Assert.Equal(indexName, result.Value.Name);

var retrieved = await indexClient.GetIndexAsync(indexName);
Assert.NotNull(retrieved);
Assert.Equal(indexName, retrieved.Value.Name);
Assert.Equal(2, retrieved.Value.Fields.Count);

// Cleanup
await indexClient.DeleteIndexAsync(indexName);
}

[Fact]
public async Task CreateOrUpdateIndex_WhenIndexExists_ShouldUpdateSchema()
{
// Arrange
const string indexName = "test-createorupdate-update";
var indexClient = factory.CreateSearchIndexClient();

await CreateIndexAsync(indexClient, indexName);

// Pull the current index and add a backward-compatible field
var existing = (await indexClient.GetIndexAsync(indexName)).Value;
var originalFieldCount = existing.Fields.Count;

existing.Fields.Add(new SearchField("Tags", SearchFieldDataType.String) { IsFilterable = true, IsStored = true });

// Act - PUT on an existing index should update its schema
var result = await indexClient.CreateOrUpdateIndexAsync(existing);

// Assert
Assert.NotNull(result);
Assert.Equal(indexName, result.Value.Name);

var retrieved = await indexClient.GetIndexAsync(indexName);
Assert.Equal(originalFieldCount + 1, retrieved.Value.Fields.Count);
Assert.Contains(retrieved.Value.Fields, f => f.Name == "Tags");

// Cleanup
await indexClient.DeleteIndexAsync(indexName);
}

[Fact]
public async Task DeleteIndex_ShouldSucceed()
{
Expand Down
41 changes: 41 additions & 0 deletions AzureSearchEmulator/Controllers/IndexesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,47 @@ public async Task<IActionResult> Post() //([FromBody] SearchIndex? index)
return Created(index);
}

[HttpPut]
[Route("indexes({key})")]
[Route("indexes/{key}")]
public async Task<IActionResult> Put(string key)
{
// Strip quotes that may be captured from OData-style URLs
key = key.Trim('\'');

// HACK.JS: For some reason, having this as a parameter with [FromBody] fails to deserialize properly.
using var sr = new StreamReader(Request.Body);
var indexJson = await sr.ReadToEndAsync();
var index = JsonSerializer.Deserialize<SearchIndex>(indexJson, jsonSerializerOptions);

if (index == null || !ModelState.IsValid)
{
return BadRequest(ModelState);
}

if (!string.Equals(index.Name, key, StringComparison.OrdinalIgnoreCase))
{
ModelState.AddModelError(nameof(index.Name), "The index name in the request body must match the name in the URL.");
return BadRequest(ModelState);
}

var existing = await searchIndexRepository.Get(key);

if (existing == null)
{
await searchIndexRepository.Create(index);
return Created(index);
}

await searchIndexRepository.Update(index);

// Clear cached Lucene resources so schema changes take effect
luceneIndexReaderFactory.ClearCachedReader(index.Name);
luceneDirectoryFactory.ClearCachedDirectory(index.Name);

return Ok(index);
}

[HttpDelete]
[Route("indexes({key})")]
[Route("indexes/{key}")]
Expand Down
14 changes: 14 additions & 0 deletions AzureSearchEmulator/Repositories/FileSearchIndexRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,20 @@ public Task Create(SearchIndex index)
return WriteAllTextAsync(file, json);
}

public Task Update(SearchIndex index)
{
if (!Directory.Exists(_options.IndexesDirectory))
{
Directory.CreateDirectory(_options.IndexesDirectory);
}

string file = GetIndexFileName(index.Name);

string json = JsonSerializer.Serialize(index, jsonSerializerOptions);

return WriteAllTextAsync(file, json);
}

public Task<bool> Delete(SearchIndex index)
{
if (!Directory.Exists(_options.IndexesDirectory))
Expand Down
2 changes: 2 additions & 0 deletions AzureSearchEmulator/Repositories/ISearchIndexRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ public interface ISearchIndexRepository

Task Create(SearchIndex index);

Task Update(SearchIndex index);

Task<bool> Delete(SearchIndex index);
}
Loading