Skip to content

Commit 25b67e6

Browse files
committed
feat(model-server): implement deleting branches via REST API
Adds a new DELETE endpoint to the REST API for deleting a branch from a repository. Garbage collection of data potentially not referenced anymore is not handled here and will be part of a later feature implementation. The tests for ModelReplicationServer have been refactored. runWithTestModelServer now provides a fixture data class to the executed block containing the objects comprising the test model-server so that tests, for instance, have access to the used repository manager.
1 parent 854f72c commit 25b67e6

File tree

3 files changed

+150
-20
lines changed

3 files changed

+150
-20
lines changed

model-server-openapi/specifications/model-server.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,24 @@ paths:
8585
"200":
8686
$ref: '#/components/responses/200'
8787
/v2/repositories/{repository}/branches/{branch}:
88+
delete:
89+
operationId: deleteRepositoryBranch
90+
parameters:
91+
- name: repository
92+
in: "path"
93+
required: true
94+
schema:
95+
type: string
96+
- name: branch
97+
in: "path"
98+
required: true
99+
schema:
100+
type: string
101+
responses:
102+
"404":
103+
$ref: '#/components/responses/404'
104+
"204":
105+
description: "Branch successfully deleted"
88106
get:
89107
operationId: getRepositoryBranch
90108
parameters:

model-server/src/main/kotlin/org/modelix/model/server/handlers/ModelReplicationServer.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import io.ktor.server.plugins.origin
2323
import io.ktor.server.request.acceptItems
2424
import io.ktor.server.request.receive
2525
import io.ktor.server.request.receiveStream
26+
import io.ktor.server.resources.delete
2627
import io.ktor.server.resources.get
2728
import io.ktor.server.resources.post
2829
import io.ktor.server.resources.put
@@ -148,6 +149,37 @@ class ModelReplicationServer(
148149
call.respondDelta(versionHash, baseVersionHash)
149150
}
150151

152+
delete<Paths.deleteRepositoryBranch> {
153+
val repositoryName = call.parameters["repository"]
154+
if (repositoryName == null) {
155+
call.respondText("Request lacks repository name", status = HttpStatusCode.BadRequest)
156+
return@delete
157+
}
158+
val repositoryId = try {
159+
RepositoryId(repositoryName)
160+
} catch (e: IllegalArgumentException) {
161+
call.respondText("Invalid repository name '$repositoryName'", status = HttpStatusCode.BadRequest)
162+
return@delete
163+
}
164+
val branch = call.parameters["branch"]
165+
if (branch == null) {
166+
call.respondText("Request lacks branch name", status = HttpStatusCode.BadRequest)
167+
return@delete
168+
}
169+
170+
if (!repositoriesManager.getBranchNames(repositoryId).contains(branch)) {
171+
call.respondText(
172+
"Repository does not exist or branch '$branch' does not exist in repository '$repositoryId'",
173+
status = HttpStatusCode.NotFound,
174+
)
175+
return@delete
176+
}
177+
178+
repositoriesManager.removeBranches(repositoryId, setOf(branch))
179+
180+
call.respond(HttpStatusCode.NoContent)
181+
}
182+
151183
get<Paths.getRepositoryBranchHash> {
152184
fun ApplicationCall.repositoryId() = RepositoryId(parameters["repository"]!!)
153185
fun PipelineContext<Unit, ApplicationCall>.repositoryId() = call.repositoryId()

model-server/src/test/kotlin/org/modelix/model/server/handlers/ModelReplicationServerTest.kt

Lines changed: 100 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import io.ktor.client.HttpClient
1919
import io.ktor.client.engine.cio.CIO
2020
import io.ktor.client.plugins.HttpTimeout
2121
import io.ktor.client.plugins.defaultRequest
22+
import io.ktor.client.request.delete
2223
import io.ktor.client.request.get
24+
import io.ktor.client.statement.bodyAsText
2325
import io.ktor.http.HttpHeaders
2426
import io.ktor.http.HttpStatusCode
2527
import io.ktor.http.appendPathSegments
@@ -51,39 +53,52 @@ import org.modelix.model.server.store.InMemoryStoreClient
5153
import org.modelix.model.server.store.LocalModelClient
5254
import org.modelix.modelql.core.assertNotEmpty
5355
import kotlin.test.Test
56+
import kotlin.test.assertContains
5457
import kotlin.test.assertEquals
58+
import kotlin.test.assertFalse
5559
import kotlin.test.fail
5660
import kotlin.time.Duration.Companion.seconds
5761

5862
class ModelReplicationServerTest {
5963

60-
private fun getDefaultModelReplicationServer(): ModelReplicationServer {
64+
private data class Fixture(
65+
val storeClient: InMemoryStoreClient,
66+
val modelClient: LocalModelClient,
67+
val repositoriesManager: IRepositoriesManager,
68+
val modelReplicationServer: ModelReplicationServer,
69+
)
70+
71+
private fun getDefaultModelReplicationServerFixture(): Fixture {
6172
val storeClient = InMemoryStoreClient()
6273
val modelClient = LocalModelClient(storeClient)
6374
val repositoriesManager = RepositoriesManager(modelClient)
64-
return ModelReplicationServer(repositoriesManager, modelClient, InMemoryModels())
75+
return Fixture(
76+
storeClient,
77+
modelClient,
78+
repositoriesManager,
79+
ModelReplicationServer(repositoriesManager, modelClient, InMemoryModels()),
80+
)
6581
}
6682

6783
private fun runWithTestModelServer(
68-
modelReplicationServer: ModelReplicationServer = getDefaultModelReplicationServer(),
69-
block: suspend ApplicationTestBuilder.(scope: CoroutineScope) -> Unit,
84+
fixture: Fixture = getDefaultModelReplicationServerFixture(),
85+
block: suspend ApplicationTestBuilder.(scope: CoroutineScope, fixture: Fixture) -> Unit,
7086
) = testApplication {
7187
application {
7288
installAuthentication(unitTestMode = true)
7389
installDefaultServerPlugins()
74-
modelReplicationServer.init(this)
90+
fixture.modelReplicationServer.init(this)
7591
}
7692

7793
coroutineScope {
78-
block(this)
94+
block(this, fixture)
7995
}
8096
}
8197

8298
@Test
83-
fun `pulling delta does not return objects twice`() = runWithTestModelServer {
99+
fun `pulling delta does not return objects twice`() = runWithTestModelServer { _, _ ->
84100
// Arrange
85-
val url = "http://localhost/v2"
86-
val modelClient = ModelClientV2.builder().url(url).client(client).build().also { it.init() }
101+
val modelClient = ModelClientV2.builder().url("v2").client(client).build().also { it.init() }
87102
val repositoryId = RepositoryId("repo1")
88103
val branchId = repositoryId.getBranchReference("my-branch")
89104
// By calling modelClient.runWrite twice, we create to versions.
@@ -98,8 +113,7 @@ class ModelReplicationServerTest {
98113
// Act
99114
val response = client.get {
100115
url {
101-
takeFrom(url)
102-
appendPathSegments("repositories", repositoryId.id, "branches", branchId.branchName)
116+
appendPathSegments("v2", "repositories", repositoryId.id, "branches", branchId.branchName)
103117
}
104118
useVersionStreamFormat()
105119
}
@@ -117,6 +131,75 @@ class ModelReplicationServerTest {
117131
}
118132
}
119133

134+
@Test
135+
fun `responds with 404 when deleting a branch from a non-existent repository`() {
136+
runWithTestModelServer { _, _ ->
137+
val response = client.delete {
138+
url {
139+
appendPathSegments("v2", "repositories", "doesnotexist", "branches", "does not exist")
140+
}
141+
}
142+
143+
assertEquals(HttpStatusCode.NotFound, response.status)
144+
assertContains(response.bodyAsText(), "does not exist in repository")
145+
}
146+
}
147+
148+
@Test
149+
fun `responds with 404 when deleting a non-existent branch`() {
150+
val repositoryId = RepositoryId("repo1")
151+
152+
runWithTestModelServer { _, fixture ->
153+
fixture.repositoriesManager.createRepository(repositoryId, null)
154+
155+
val response = client.delete {
156+
url {
157+
appendPathSegments("v2", "repositories", repositoryId.id, "branches", "does not exist")
158+
}
159+
}
160+
161+
assertEquals(HttpStatusCode.NotFound, response.status)
162+
assertContains(response.bodyAsText(), "does not exist in repository")
163+
}
164+
}
165+
166+
@Test
167+
fun `responds with 400 when deleting from an invalid repository ID`() {
168+
runWithTestModelServer { _, fixture ->
169+
val response = client.delete {
170+
url {
171+
appendPathSegments("v2", "repositories", "invalid with spaces", "branches", "master")
172+
}
173+
}
174+
assertEquals(HttpStatusCode.BadRequest, response.status)
175+
assertContains(response.bodyAsText(), "Invalid repository name")
176+
}
177+
}
178+
179+
@Test
180+
fun `successfully deletes existing branches`() {
181+
val repositoryId = RepositoryId("repo1")
182+
val branch = "testbranch"
183+
val defaultBranchRef = repositoryId.getBranchReference("master")
184+
185+
runWithTestModelServer { _, fixture ->
186+
fixture.repositoriesManager.createRepository(repositoryId, null)
187+
fixture.repositoriesManager.mergeChanges(
188+
repositoryId.getBranchReference(branch),
189+
checkNotNull(fixture.repositoriesManager.getVersionHash(defaultBranchRef)) { "Default branch must exist" },
190+
)
191+
192+
val response = client.delete {
193+
url {
194+
appendPathSegments("v2", "repositories", repositoryId.id, "branches", branch)
195+
}
196+
}
197+
198+
assertEquals(HttpStatusCode.NoContent, response.status)
199+
assertFalse(fixture.repositoriesManager.getBranchNames(repositoryId).contains(branch))
200+
}
201+
}
202+
120203
@Test
121204
fun `server responds with error when failing to compute delta before starting to respond`() {
122205
// Arrange
@@ -130,18 +213,16 @@ class ModelReplicationServerTest {
130213
}
131214
}
132215
val modelReplicationServer = ModelReplicationServer(faultyRepositoriesManager, modelClient, InMemoryModels())
133-
val url = "http://localhost/v2"
134216
val repositoryId = RepositoryId("repo1")
135217
val branchRef = repositoryId.getBranchReference()
136218

137-
runWithTestModelServer(modelReplicationServer) {
219+
runWithTestModelServer(Fixture(storeClient, modelClient, faultyRepositoriesManager, modelReplicationServer)) { _, _ ->
138220
repositoriesManager.createRepository(repositoryId, null)
139221

140222
// Act
141223
val response = client.get {
142224
url {
143-
takeFrom(url)
144-
appendPathSegments("repositories", repositoryId.id, "branches", branchRef.branchName)
225+
appendPathSegments("v2", "repositories", repositoryId.id, "branches", branchRef.branchName)
145226
}
146227
useVersionStreamFormat()
147228
}
@@ -210,10 +291,10 @@ class ModelReplicationServerTest {
210291
runWithNettyServer(setupBlock, testBlock)
211292
}
212293

213-
fun `client can pull versions in legacy version delta format`() = runWithTestModelServer {
294+
@Test
295+
fun `client can pull versions in legacy version delta format`() = runWithTestModelServer { _, _ ->
214296
// Arrange
215-
val url = "http://localhost/v2"
216-
val modelClient = ModelClientV2.builder().url(url).client(client).build().also { it.init() }
297+
val modelClient = ModelClientV2.builder().url("v2").client(client).build().also { it.init() }
217298
val repositoryId = RepositoryId("repo1")
218299
val branchId = repositoryId.getBranchReference("my-branch")
219300
modelClient.runWrite(branchId) { root ->
@@ -224,8 +305,7 @@ class ModelReplicationServerTest {
224305
val response = client.get {
225306
headers[HttpHeaders.Accept] = VersionDeltaStream.CONTENT_TYPE.toString()
226307
url {
227-
takeFrom(url)
228-
appendPathSegments("repositories", repositoryId.id, "branches", branchId.branchName)
308+
appendPathSegments("v2", "repositories", repositoryId.id, "branches", branchId.branchName)
229309
}
230310
}
231311
val versionDelta = response.readVersionDelta()

0 commit comments

Comments
 (0)