feat(taiga): Status zurückschreiben — zwei Knöpfe, niemand gewinnt von selbst (D91-Nachtrag 8, SPEC §9)
Weicht der Ticket-Status von der Statusbox ab, markiert das Knoten-Fenster
die Abweichung und bietet beide Richtungen ausdrücklich an.
- „nach Taiga schreiben": Spalte des Projekts suchen (Taiga schreibt nach Id,
die Namen sind je Projekt frei) und mit der zuletzt GELESENEN `version`
patchen — hat jemand dazwischen geändert, lehnt Taiga ab und der Text steht
im Fenster, statt dass etwas überschrieben wird.
- „aus Taiga übernehmen": `setStatusBox()` schreibt die Box in die Textzeile,
undo-fähig wie jede andere Änderung.
- Schreibbar sind nur die fünf abgebildeten Zustände; `[?]`, `[!]`, `[-]` und
der neutrale Knoten lassen das Ticket unangetastet — mit Begründung im
Fenster.
- Proxy: zwei Spaltenlisten (`/taiga/{userstory,task}-statuses?slug=`) und
zwei Schreib-Endpunkte (`PATCH …/{ref}/status?slug=`); die Zielspalte wählt
der Editor, das Backend parst die Notation nicht (D14).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4e6f98bbee
commit
323e8fe0ba
@@ -81,3 +81,10 @@ docs/SPEC.md §10 testen — niemals eine zweite, abweichende Grammatik pflegen.
|
||||
deshalb den `slug` (aus `&taiga.<slug>`, SPEC §1) und fragen erst
|
||||
`/projects/by_slug`, dann `by_ref` — der Slug kommt vom Client und wird
|
||||
**kodiert** angehängt, sonst hängte ein `&` darin einen weiteren Filter an.
|
||||
- **Schreiben (D91-Nachtrag 8):** `PATCH /taiga/{userstories|tasks}/{ref}/status`
|
||||
nimmt die Status-**Id** (aus `GET /taiga/{userstory|task}-statuses?slug=`)
|
||||
und die zuletzt gelesene `version` — Taigas optimistische Sperre; ein
|
||||
Konflikt wird durchgereicht, nie überschrieben. Die Zielspalte wählt der
|
||||
Editor: Namen sind je Projekt frei, die Abbildung ist Notation (D14). Der
|
||||
Test-Stub muss **chunked** Bodies lesen (der JDK-HttpClient sendet ohne
|
||||
`Content-Length`) — sonst meldet er einen Versionskonflikt, den es nicht gibt.
|
||||
|
||||
@@ -6,9 +6,12 @@ import de.werkbaum.generated.model.TaigaProject
|
||||
import de.werkbaum.generated.model.TaigaSession
|
||||
import de.werkbaum.generated.model.TaigaStoryCreateRequest
|
||||
import de.werkbaum.generated.model.TaigaTaskCreateRequest
|
||||
import de.werkbaum.generated.model.TaigaStatus
|
||||
import de.werkbaum.generated.model.TaigaStatusPatch
|
||||
import de.werkbaum.generated.model.TaigaTicket
|
||||
import de.werkbaum.generated.model.TaigaTicketDetail
|
||||
import de.werkbaum.integration.taiga.TaigaClient
|
||||
import de.werkbaum.integration.taiga.TaigaTicketDetailData
|
||||
import de.werkbaum.integration.taiga.TaigaTicketData
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
@@ -84,19 +87,60 @@ class TaigaController(private val client: TaigaClient) : TaigaApi {
|
||||
ticket(xTaigaToken, slug, ref, task = true)
|
||||
|
||||
private fun ticket(token: String, slug: String, ref: Long, task: Boolean):
|
||||
ResponseEntity<TaigaTicketDetail> {
|
||||
val d = client.ticket(token, slug, ref, task)
|
||||
return ResponseEntity.ok(
|
||||
TaigaTicketDetail(
|
||||
id = d.id,
|
||||
ref = d.ref,
|
||||
subject = d.subject,
|
||||
status = d.status,
|
||||
statusClosed = d.statusClosed,
|
||||
assignee = d.assignee,
|
||||
)
|
||||
ResponseEntity<TaigaTicketDetail> =
|
||||
ResponseEntity.ok(client.ticket(token, slug, ref, task).toApi())
|
||||
|
||||
/* Schreiben (D91-Nachtrag 7/8): angestoßen wird es im Knoten-Fenster, von
|
||||
selbst geschieht nichts. Die Zielspalte kommt als Id herein — welche es
|
||||
ist, entscheidet der Editor über die Statusbox-Abbildung (D14). */
|
||||
override fun taigaStoryStatuses(xTaigaToken: String, slug: String) =
|
||||
statuses(xTaigaToken, slug, task = false)
|
||||
|
||||
override fun taigaTaskStatuses(xTaigaToken: String, slug: String) =
|
||||
statuses(xTaigaToken, slug, task = true)
|
||||
|
||||
private fun statuses(token: String, slug: String, task: Boolean):
|
||||
ResponseEntity<List<TaigaStatus>> =
|
||||
ResponseEntity.ok(
|
||||
client.statuses(token, slug, task).map {
|
||||
TaigaStatus(id = it.id, name = it.name, closed = it.closed)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun taigaSetStoryStatus(
|
||||
xTaigaToken: String,
|
||||
ref: Long,
|
||||
slug: String,
|
||||
taigaStatusPatch: TaigaStatusPatch,
|
||||
) = setStatus(xTaigaToken, slug, ref, task = false, patch = taigaStatusPatch)
|
||||
|
||||
override fun taigaSetTaskStatus(
|
||||
xTaigaToken: String,
|
||||
ref: Long,
|
||||
slug: String,
|
||||
taigaStatusPatch: TaigaStatusPatch,
|
||||
) = setStatus(xTaigaToken, slug, ref, task = true, patch = taigaStatusPatch)
|
||||
|
||||
private fun setStatus(
|
||||
token: String,
|
||||
slug: String,
|
||||
ref: Long,
|
||||
task: Boolean,
|
||||
patch: TaigaStatusPatch,
|
||||
): ResponseEntity<TaigaTicketDetail> =
|
||||
ResponseEntity.ok(
|
||||
client.setStatus(token, slug, ref, task, patch.status, patch.version).toApi()
|
||||
)
|
||||
|
||||
private fun TaigaTicketDetailData.toApi() = TaigaTicketDetail(
|
||||
id = id,
|
||||
ref = ref,
|
||||
subject = subject,
|
||||
status = status,
|
||||
statusClosed = statusClosed,
|
||||
assignee = assignee,
|
||||
version = version,
|
||||
)
|
||||
|
||||
private fun created(ticket: TaigaTicketData): ResponseEntity<TaigaTicket> =
|
||||
ResponseEntity.status(HttpStatus.CREATED).body(
|
||||
|
||||
@@ -51,8 +51,13 @@ data class TaigaTicketDetailData(
|
||||
val status: String?,
|
||||
val statusClosed: Boolean?,
|
||||
val assignee: String?,
|
||||
/** Taigas optimistische Sperre; geht beim Schreiben unverändert zurück. */
|
||||
val version: Long?,
|
||||
)
|
||||
|
||||
/** Eine Spalte des Projekt-Workflows (D91-Nachtrag 8). */
|
||||
data class TaigaStatusData(val id: Long, val name: String, val closed: Boolean?)
|
||||
|
||||
/**
|
||||
* Schmaler, benannter Client zur konfigurierten Taiga-Instanz (D91) — kein
|
||||
* Durchreich-Proxy: genau die vier Aufrufe, die die Ticket-Anlage braucht.
|
||||
@@ -127,16 +132,65 @@ class TaigaClient(private val properties: TaigaProperties) {
|
||||
.header("Authorization", "Bearer $token")
|
||||
.retrieve().body(MAP)
|
||||
} ?: throw TaigaUnavailableException("Leere Antwort von Taiga ($pfad)")
|
||||
return TaigaTicketDetailData(
|
||||
id = num(map, "id"),
|
||||
ref = num(map, "ref"),
|
||||
subject = str(map, "subject"),
|
||||
status = extra(map, "status_extra_info")?.get("name") as? String,
|
||||
statusClosed = extra(map, "status_extra_info")?.get("is_closed") as? Boolean,
|
||||
assignee = extra(map, "assigned_to_extra_info")?.get("full_name_display") as? String,
|
||||
)
|
||||
return detail(map)
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Spalten des Projekt-Workflows (D91-Nachtrag 8) — Taiga schreibt nach
|
||||
* Status-**Id**, und die Namen sind je Projekt frei. Welche Spalte gemeint
|
||||
* ist, entscheidet der Editor: Er kennt die Abbildung auf die Statusbox
|
||||
* (SPEC §4), das Backend parst die Notation nicht (D14).
|
||||
*/
|
||||
fun statuses(token: String, slug: String, task: Boolean): List<TaigaStatusData> {
|
||||
val project = projectId(token, slug)
|
||||
val pfad = if (task) "/task-statuses" else "/userstory-statuses"
|
||||
val list = exchange {
|
||||
rest.get().uri(url("$pfad?project=$project"))
|
||||
.header("Authorization", "Bearer $token")
|
||||
.retrieve().body(LIST)
|
||||
} ?: emptyList()
|
||||
return list.map {
|
||||
TaigaStatusData(num(it, "id"), str(it, "name"), it["is_closed"] as? Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Den Status eines Tickets setzen (D91-Nachtrag 8). Die `version` kommt
|
||||
* vom Client — sie ist die, die er gelesen hat: Taigas optimistische
|
||||
* Sperre lehnt das Schreiben ab, wenn jemand dazwischen geändert hat, und
|
||||
* der Konflikt wird durchgereicht statt überschrieben (dieselbe Haltung
|
||||
* wie beim Live-Editing, D76).
|
||||
*/
|
||||
fun setStatus(
|
||||
token: String,
|
||||
slug: String,
|
||||
ref: Long,
|
||||
task: Boolean,
|
||||
status: Long,
|
||||
version: Long,
|
||||
): TaigaTicketDetailData {
|
||||
val id = ticket(token, slug, ref, task).id
|
||||
val pfad = if (task) "/tasks/$id" else "/userstories/$id"
|
||||
val map = exchange {
|
||||
rest.patch().uri(url(pfad))
|
||||
.header("Authorization", "Bearer $token")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(mapOf("status" to status, "version" to version))
|
||||
.retrieve().body(MAP)
|
||||
} ?: throw TaigaUnavailableException("Leere Antwort von Taiga ($pfad)")
|
||||
return detail(map)
|
||||
}
|
||||
|
||||
private fun detail(map: Map<String, Any?>) = TaigaTicketDetailData(
|
||||
id = num(map, "id"),
|
||||
ref = num(map, "ref"),
|
||||
subject = str(map, "subject"),
|
||||
status = extra(map, "status_extra_info")?.get("name") as? String,
|
||||
statusClosed = extra(map, "status_extra_info")?.get("is_closed") as? Boolean,
|
||||
assignee = extra(map, "assigned_to_extra_info")?.get("full_name_display") as? String,
|
||||
version = (map["version"] as? Number)?.toLong(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Projekt-Slug -> Id; Taigas `by_ref` filtert über die Id. Der Slug kommt
|
||||
* vom Client und wird deshalb **kodiert** in die Anfrage gesetzt — sonst
|
||||
|
||||
@@ -606,6 +606,173 @@ paths:
|
||||
"503":
|
||||
$ref: "#/components/responses/TaigaNotConfigured"
|
||||
|
||||
/taiga/userstories/{ref}/status:
|
||||
patch:
|
||||
tags: [Taiga]
|
||||
operationId: taigaSetStoryStatus
|
||||
summary: Status einer User Story setzen (Proxy)
|
||||
description: >
|
||||
Schreibt den Status zurueck (D91-Nachtrag 7/8) - die eine Haelfte des
|
||||
Abgleichs, die der Benutzer im Knoten-Fenster ausdruecklich anstoesst;
|
||||
von selbst geschieht nichts. `status` ist die **Id** einer Spalte aus
|
||||
`GET /taiga/userstory-statuses` (die Namen sind je Projekt frei),
|
||||
`version` die zuletzt gelesene: Hat jemand dazwischen geaendert, lehnt
|
||||
Taigas optimistische Sperre ab und der Konflikt wird durchgereicht,
|
||||
statt ihn zu ueberschreiben.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TaigaToken"
|
||||
- $ref: "#/components/parameters/TaigaRef"
|
||||
- $ref: "#/components/parameters/TaigaSlug"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TaigaStatusPatch"
|
||||
responses:
|
||||
"200":
|
||||
description: Der neue Stand des Tickets
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TaigaTicketDetail"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
description: Token fehlt oder ist abgelaufen
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProblemDetail"
|
||||
"404":
|
||||
description: Projekt oder Ref gibt es nicht
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProblemDetail"
|
||||
"502":
|
||||
$ref: "#/components/responses/TaigaUnavailable"
|
||||
"503":
|
||||
$ref: "#/components/responses/TaigaNotConfigured"
|
||||
|
||||
/taiga/tasks/{ref}/status:
|
||||
patch:
|
||||
tags: [Taiga]
|
||||
operationId: taigaSetTaskStatus
|
||||
summary: Status einer Task setzen (Proxy)
|
||||
description: >
|
||||
Wie `PATCH /taiga/userstories/{ref}/status`, nur fuer Tasks; die
|
||||
Spalten kommen aus `GET /taiga/task-statuses`.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TaigaToken"
|
||||
- $ref: "#/components/parameters/TaigaRef"
|
||||
- $ref: "#/components/parameters/TaigaSlug"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TaigaStatusPatch"
|
||||
responses:
|
||||
"200":
|
||||
description: Der neue Stand des Tickets
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TaigaTicketDetail"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
description: Token fehlt oder ist abgelaufen
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProblemDetail"
|
||||
"404":
|
||||
description: Projekt oder Ref gibt es nicht
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProblemDetail"
|
||||
"502":
|
||||
$ref: "#/components/responses/TaigaUnavailable"
|
||||
"503":
|
||||
$ref: "#/components/responses/TaigaNotConfigured"
|
||||
|
||||
/taiga/userstory-statuses:
|
||||
get:
|
||||
tags: [Taiga]
|
||||
operationId: taigaStoryStatuses
|
||||
summary: Workflow-Spalten der Storys eines Projekts (Proxy)
|
||||
description: >
|
||||
`GET <api-url>/userstory-statuses?project=<id>`. Gebraucht zum
|
||||
Schreiben: Taiga nimmt die Status-**Id**, und welche Spalte zu welcher
|
||||
Statusbox gehoert, entscheidet der Editor (SPEC par. 4/9) - das
|
||||
Backend parst die Notation nicht (D14).
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TaigaToken"
|
||||
- $ref: "#/components/parameters/TaigaSlug"
|
||||
responses:
|
||||
"200":
|
||||
description: Die Spalten des Projekts
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/TaigaStatus"
|
||||
"401":
|
||||
description: Token fehlt oder ist abgelaufen
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProblemDetail"
|
||||
"404":
|
||||
description: Projekt gibt es nicht
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProblemDetail"
|
||||
"502":
|
||||
$ref: "#/components/responses/TaigaUnavailable"
|
||||
"503":
|
||||
$ref: "#/components/responses/TaigaNotConfigured"
|
||||
|
||||
/taiga/task-statuses:
|
||||
get:
|
||||
tags: [Taiga]
|
||||
operationId: taigaTaskStatuses
|
||||
summary: Workflow-Spalten der Tasks eines Projekts (Proxy)
|
||||
description: Wie `GET /taiga/userstory-statuses`, nur fuer Tasks.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TaigaToken"
|
||||
- $ref: "#/components/parameters/TaigaSlug"
|
||||
responses:
|
||||
"200":
|
||||
description: Die Spalten des Projekts
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/TaigaStatus"
|
||||
"401":
|
||||
description: Token fehlt oder ist abgelaufen
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProblemDetail"
|
||||
"404":
|
||||
description: Projekt gibt es nicht
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProblemDetail"
|
||||
"502":
|
||||
$ref: "#/components/responses/TaigaUnavailable"
|
||||
"503":
|
||||
$ref: "#/components/responses/TaigaNotConfigured"
|
||||
|
||||
/info:
|
||||
get:
|
||||
tags: [Documents]
|
||||
@@ -1113,6 +1280,42 @@ components:
|
||||
Anzeigename des Zustaendigen
|
||||
(`assigned_to_extra_info.full_name_display`); fehlt, wenn niemand
|
||||
zugewiesen ist.
|
||||
version:
|
||||
type: integer
|
||||
format: int64
|
||||
description: >
|
||||
Taigas optimistische Sperre. Sie geht beim Schreiben unveraendert
|
||||
zurueck (`PATCH .../status`); passt sie nicht mehr, lehnt Taiga ab
|
||||
und der Konflikt wird durchgereicht.
|
||||
|
||||
TaigaStatus:
|
||||
type: object
|
||||
description: Eine Spalte des Projekt-Workflows (D91-Nachtrag 8).
|
||||
required: [id, name]
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
name:
|
||||
type: string
|
||||
description: >
|
||||
Frei benannt, je Projekt verschieden - die Zuordnung zur Statusbox
|
||||
der Notation macht der Editor.
|
||||
closed:
|
||||
type: boolean
|
||||
|
||||
TaigaStatusPatch:
|
||||
type: object
|
||||
required: [status, version]
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Id der Zielspalte (aus den Status-Listen), nicht ihr Name.
|
||||
version:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Die zuletzt gelesene Version des Tickets.
|
||||
|
||||
ProblemDetail:
|
||||
type: object
|
||||
|
||||
@@ -143,6 +143,31 @@ class TaigaApiTest {
|
||||
result.responseBody!! shouldContain "\"assignee\":null"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `die Workflow-Spalten eines Projekts kommen als Id und Name`() {
|
||||
val result = client.get()
|
||||
.uri("/api/v1/taiga/userstory-statuses?slug=mi-kunde")
|
||||
.header("X-Taiga-Token", "tok-abc123")
|
||||
.exchange()
|
||||
.returnResult(String::class.java)
|
||||
result.status.value() shouldBe 200
|
||||
result.responseBody!! shouldContain """{"id":12,"name":"In progress","closed":false}"""
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ein Status wird per Ref gesetzt und antwortet mit dem neuen Stand`() {
|
||||
val result = client.patch()
|
||||
.uri("/api/v1/taiga/userstories/123/status?slug=mi-kunde")
|
||||
.header("X-Taiga-Token", "tok-abc123")
|
||||
.header("Content-Type", "application/json")
|
||||
.body("""{"status":13,"version":7}""")
|
||||
.exchange()
|
||||
.returnResult(String::class.java)
|
||||
result.status.value() shouldBe 200
|
||||
result.responseBody!! shouldContain "\"status\":\"Done\""
|
||||
result.responseBody!! shouldContain "\"version\":8"
|
||||
}
|
||||
|
||||
companion object {
|
||||
private lateinit var stub: HttpServer
|
||||
@Volatile private var stubStatus = 200
|
||||
@@ -160,6 +185,9 @@ class TaigaApiTest {
|
||||
"/api/v1/projects/by_slug" -> TaigaClientTestData.PROJECT_OK
|
||||
"/api/v1/userstories/by_ref" -> TaigaClientTestData.STORY_DETAIL
|
||||
"/api/v1/tasks/by_ref" -> TaigaClientTestData.TASK_DETAIL
|
||||
"/api/v1/userstory-statuses", "/api/v1/task-statuses" ->
|
||||
TaigaClientTestData.STATUSES_OK
|
||||
"/api/v1/userstories/1234" -> TaigaClientTestData.STORY_PATCHED
|
||||
else -> "{}"
|
||||
}
|
||||
val status = if (stubBody != null) stubStatus
|
||||
@@ -209,9 +237,16 @@ object TaigaClientTestData {
|
||||
const val PROJECT_OK =
|
||||
"""{"id": 7, "name": "Kunde", "slug": "mi-kunde"}"""
|
||||
const val STORY_DETAIL =
|
||||
"""{"id": 1234, "ref": 123, "subject": "Login bauen", "project": 7,
|
||||
"""{"id": 1234, "ref": 123, "subject": "Login bauen", "project": 7, "version": 7,
|
||||
"status_extra_info": {"name": "In progress", "is_closed": false},
|
||||
"assigned_to_extra_info": {"full_name_display": "Anna Beispiel"}}"""
|
||||
const val STATUSES_OK =
|
||||
"""[{"id": 11, "name": "New", "is_closed": false},
|
||||
{"id": 12, "name": "In progress", "is_closed": false},
|
||||
{"id": 13, "name": "Done", "is_closed": true}]"""
|
||||
const val STORY_PATCHED =
|
||||
"""{"id": 1234, "ref": 123, "subject": "Login bauen", "version": 8,
|
||||
"status_extra_info": {"name": "Done", "is_closed": true}}"""
|
||||
const val TASK_DETAIL =
|
||||
"""{"id": 5678, "ref": 1234, "subject": "API-Teil", "project": 7,
|
||||
"status_extra_info": {"name": "Done", "is_closed": true}}"""
|
||||
|
||||
@@ -162,6 +162,64 @@ class TaigaClientTest {
|
||||
requests[0].query shouldBe "slug=a%26member%3D1"
|
||||
}
|
||||
|
||||
/* ---- Status zurückschreiben (D91-Nachtrag 7/8) ---- */
|
||||
|
||||
@Test
|
||||
fun `statuses liefert die Spalten des Projekt-Workflows`() {
|
||||
routes["/api/v1/projects/by_slug"] = 200 to PROJECT_OK
|
||||
routes["/api/v1/userstory-statuses"] = 200 to STATUSES_OK
|
||||
val s = client().statuses("tok-abc123", "mi-kunde", task = false)
|
||||
|
||||
s.map { it.name } shouldBe listOf("New", "In progress", "Done")
|
||||
s[0].id shouldBe 11L
|
||||
s[2].closed shouldBe true
|
||||
requests[1].query shouldBe "project=7"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Task-Spalten kommen vom eigenen Endpunkt`() {
|
||||
routes["/api/v1/projects/by_slug"] = 200 to PROJECT_OK
|
||||
routes["/api/v1/task-statuses"] = 200 to STATUSES_OK
|
||||
client().statuses("tok-abc123", "mi-kunde", task = true)
|
||||
requests[1].path shouldBe "/api/v1/task-statuses"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setStatus loest die Ref auf und patcht mit Status-Id und Version`() {
|
||||
routes["/api/v1/projects/by_slug"] = 200 to PROJECT_OK
|
||||
routes["/api/v1/userstories/by_ref"] = 200 to STORY_DETAIL
|
||||
routes["/api/v1/userstories/1234"] = 200 to STORY_PATCHED
|
||||
val d = client().setStatus("tok-abc123", "mi-kunde", 123, task = false, status = 13, version = 7)
|
||||
|
||||
d.status shouldBe "Done"
|
||||
d.version shouldBe 8L
|
||||
|
||||
val patch = requests.last()
|
||||
patch.path shouldBe "/api/v1/userstories/1234" /* per Id, nicht per Ref */
|
||||
patch.method shouldBe "PATCH"
|
||||
patch.body shouldContain "\"status\":13"
|
||||
patch.body shouldContain "\"version\":7"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `eine veraltete Version wird als Konflikt durchgereicht`() {
|
||||
routes["/api/v1/projects/by_slug"] = 200 to PROJECT_OK
|
||||
routes["/api/v1/userstories/by_ref"] = 200 to STORY_DETAIL
|
||||
routes["/api/v1/userstories/1234"] = 400 to STALE
|
||||
val ex = shouldThrow<TaigaUpstreamException> {
|
||||
client().setStatus("tok-abc123", "mi-kunde", 123, task = false, status = 13, version = 1)
|
||||
}
|
||||
ex.status shouldBe 400
|
||||
ex.message shouldContain "modified"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `die gelesene Version kommt mit - sie ist die Sperre fuers Schreiben`() {
|
||||
routes["/api/v1/projects/by_slug"] = 200 to PROJECT_OK
|
||||
routes["/api/v1/userstories/by_ref"] = 200 to STORY_DETAIL
|
||||
client().ticket("tok-abc123", "mi-kunde", 123, task = false).version shouldBe 7L
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ohne konfigurierte Instanz gibt es kein Ziel`() {
|
||||
val bare = TaigaClient(TaigaProperties(apiUrl = ""))
|
||||
@@ -183,6 +241,7 @@ class TaigaClientTest {
|
||||
|
||||
data class Recorded(
|
||||
val path: String,
|
||||
val method: String,
|
||||
val query: String?,
|
||||
val auth: String?,
|
||||
val noPagination: String?,
|
||||
@@ -220,7 +279,7 @@ class TaigaClientTest {
|
||||
/* Form der by_ref-Antwort: die Namen stehen in den
|
||||
`*_extra_info`-Blöcken, die Ids daneben (Taiga-API). */
|
||||
const val STORY_DETAIL =
|
||||
"""{"id": 1234, "ref": 123, "subject": "Login bauen", "project": 7, "status": 3,
|
||||
"""{"id": 1234, "ref": 123, "subject": "Login bauen", "project": 7, "status": 3, "version": 7,
|
||||
"status_extra_info": {"name": "In progress", "color": "#ff9900", "is_closed": false},
|
||||
"assigned_to": 42,
|
||||
"assigned_to_extra_info": {"username": "anna", "full_name_display": "Anna Beispiel"}}"""
|
||||
@@ -230,6 +289,15 @@ class TaigaClientTest {
|
||||
"assigned_to_extra_info": null}"""
|
||||
const val STORY_BARE =
|
||||
"""{"id": 1234, "ref": 123, "subject": "Login bauen", "project": 7, "status": 3}"""
|
||||
const val STATUSES_OK =
|
||||
"""[{"id": 11, "name": "New", "is_closed": false, "order": 1},
|
||||
{"id": 12, "name": "In progress", "is_closed": false},
|
||||
{"id": 13, "name": "Done", "is_closed": true}]"""
|
||||
const val STORY_PATCHED =
|
||||
"""{"id": 1234, "ref": 123, "subject": "Login bauen", "version": 8,
|
||||
"status_extra_info": {"name": "Done", "is_closed": true}}"""
|
||||
const val STALE =
|
||||
"""{"_error_message": "The version doesn't match the data base version. The object was modified.", "_error_type": "taiga.base.exceptions.WrongArguments"}"""
|
||||
const val NOT_FOUND =
|
||||
"""{"_error_message": "Not found.", "_error_type": "taiga.base.exceptions.NotFound"}"""
|
||||
|
||||
@@ -240,6 +308,7 @@ class TaigaClientTest {
|
||||
server.createContext("/") { ex ->
|
||||
recorded = Recorded(
|
||||
path = ex.requestURI.path,
|
||||
method = ex.requestMethod,
|
||||
query = ex.requestURI.query,
|
||||
auth = ex.requestHeaders.getFirst("Authorization"),
|
||||
noPagination = ex.requestHeaders.getFirst("x-disable-pagination"),
|
||||
|
||||
Reference in New Issue
Block a user