first commit
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/dto"
|
||||
"browser.local/platform/validator"
|
||||
)
|
||||
|
||||
func TestArtifactTransferAPIWorkflow(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
hello := createArtifactTransferAPIFixtures(t, router)
|
||||
payload := []byte("artifact payload for api upload")
|
||||
openRequest := validArtifactTransferOpenRequest(hello.SessionToken, payload, 8)
|
||||
|
||||
openRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/open", openRequest)
|
||||
assertStatus(t, openRecorder, http.StatusOK)
|
||||
opened := decodeBody[dto.ArtifactTransferOpenResponse](t, openRecorder)
|
||||
if !opened.Accepted || opened.TransferID == "" || opened.TotalChunks != 4 || opened.Artifact.State != domain.ArtifactStateUploading {
|
||||
t.Fatalf("unexpected open response: %+v", opened)
|
||||
}
|
||||
|
||||
chunkRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/chunks", validArtifactChunkRequest(hello.SessionToken, opened.TransferID, payload, 0, 8))
|
||||
assertStatus(t, chunkRecorder, http.StatusOK)
|
||||
chunk := decodeBody[dto.ArtifactChunkUploadResponse](t, chunkRecorder)
|
||||
if !chunk.Accepted || chunk.NextMissingChunkIndex != 1 || len(chunk.ReceivedChunkIndexes) != 1 {
|
||||
t.Fatalf("unexpected chunk response: %+v", chunk)
|
||||
}
|
||||
|
||||
duplicateRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/chunks", validArtifactChunkRequest(hello.SessionToken, opened.TransferID, payload, 0, 8))
|
||||
assertStatus(t, duplicateRecorder, http.StatusOK)
|
||||
duplicate := decodeBody[dto.ArtifactChunkUploadResponse](t, duplicateRecorder)
|
||||
if !duplicate.Duplicate {
|
||||
t.Fatalf("expected duplicate chunk ack, got %+v", duplicate)
|
||||
}
|
||||
|
||||
statusRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/status", dto.ArtifactTransferStatusRequest{RunEndpointID: "run-local", SessionToken: hello.SessionToken, TransferID: opened.TransferID, ArtifactID: "artifact-1"})
|
||||
assertStatus(t, statusRecorder, http.StatusOK)
|
||||
status := decodeBody[dto.ArtifactTransferStatusResponse](t, statusRecorder)
|
||||
if status.NextMissingChunkIndex != 1 || len(status.ReceivedChunkIndexes) != 1 {
|
||||
t.Fatalf("unexpected status response: %+v", status)
|
||||
}
|
||||
|
||||
missingComplete := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/complete", dto.ArtifactTransferCompleteRequest{RunEndpointID: "run-local", SessionToken: hello.SessionToken, TransferID: opened.TransferID, ArtifactID: "artifact-1", Checksum: validator.BytesChecksum(payload), SizeBytes: int64(len(payload))})
|
||||
assertErrorResponse(t, missingComplete, http.StatusBadRequest, errorCodeValidation)
|
||||
|
||||
for index := 1; index < opened.TotalChunks; index++ {
|
||||
partRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/chunks", validArtifactChunkRequest(hello.SessionToken, opened.TransferID, payload, index, 8))
|
||||
assertStatus(t, partRecorder, http.StatusOK)
|
||||
}
|
||||
completeRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/complete", dto.ArtifactTransferCompleteRequest{RunEndpointID: "run-local", SessionToken: hello.SessionToken, TransferID: opened.TransferID, ArtifactID: "artifact-1", Checksum: validator.BytesChecksum(payload), SizeBytes: int64(len(payload))})
|
||||
assertStatus(t, completeRecorder, http.StatusOK)
|
||||
complete := decodeBody[dto.ArtifactTransferCompleteResponse](t, completeRecorder)
|
||||
if !complete.Accepted || !complete.Completed || complete.Artifact.State != domain.ArtifactStateAvailable {
|
||||
t.Fatalf("unexpected complete response: %+v", complete)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactTransferAPIErrors(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
hello := createArtifactTransferAPIFixtures(t, router)
|
||||
payload := []byte("artifact payload")
|
||||
openRequest := validArtifactTransferOpenRequest(hello.SessionToken, payload, 8)
|
||||
opened := decodeBody[dto.ArtifactTransferOpenResponse](t, performArtifactTransferOpen(t, router, openRequest))
|
||||
|
||||
badChunk := validArtifactChunkRequest(hello.SessionToken, opened.TransferID, payload, 0, 8)
|
||||
badChunk.Checksum = validator.BytesChecksum([]byte("different"))
|
||||
badChunkRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/chunks", badChunk)
|
||||
assertErrorResponse(t, badChunkRecorder, http.StatusBadRequest, errorCodeValidation)
|
||||
|
||||
invalidSession := validArtifactTransferOpenRequest("stale-token", payload, 8)
|
||||
invalidSession.ArtifactID = "artifact-invalid-session"
|
||||
invalidSession.IdempotencyKey = "artifact-invalid-session"
|
||||
invalidSessionRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/open", invalidSession)
|
||||
assertErrorResponse(t, invalidSessionRecorder, http.StatusBadRequest, errorCodeValidation)
|
||||
|
||||
invalidOwner := validArtifactTransferOpenRequest(hello.SessionToken, payload, 8)
|
||||
invalidOwner.OwnerKind = domain.ArtifactOwnerKindPlatform
|
||||
invalidOwner.ArtifactID = "artifact-invalid-owner"
|
||||
invalidOwner.IdempotencyKey = "artifact-invalid-owner"
|
||||
invalidOwnerRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/open", invalidOwner)
|
||||
assertErrorResponse(t, invalidOwnerRecorder, http.StatusBadRequest, errorCodeValidation)
|
||||
|
||||
methodFailure := performRaw(t, router, http.MethodGet, "/api/v1/run/artifacts/open", "")
|
||||
assertErrorResponse(t, methodFailure, http.StatusMethodNotAllowed, errorCodeMethodNotAllowed)
|
||||
}
|
||||
|
||||
func createArtifactTransferAPIFixtures(t *testing.T, router http.Handler) dto.RunControlHelloResponse {
|
||||
t.Helper()
|
||||
helloRequest := validRunControlHelloRequest()
|
||||
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, "process.install", "process.start", "process.stop", "logs.read", "files.read")
|
||||
helloRequest.CapabilityReport.Fingerprint = "cap-artifacts"
|
||||
hello := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, helloRequest))
|
||||
adminSession := createAdminSession(t, router)
|
||||
postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins", validGamePluginRequest())
|
||||
postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{ID: "server-1", PluginID: "server.scum", RunEndpointID: "run-local", Name: "SCUM #1"}, adminSession)
|
||||
postJSON[dto.JobResponse](t, router, "/api/v1/jobs", dto.JobCreateRequest{ID: "job-1", ServerInstanceID: "server-1", RunEndpointID: "run-local", Capability: "process.start", IdempotencyKey: "idem-start"})
|
||||
return hello
|
||||
}
|
||||
|
||||
func performArtifactTransferOpen(t *testing.T, router http.Handler, request dto.ArtifactTransferOpenRequest) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
recorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/open", request)
|
||||
assertStatus(t, recorder, http.StatusOK)
|
||||
return recorder
|
||||
}
|
||||
|
||||
func validArtifactTransferOpenRequest(sessionToken string, payload []byte, chunkSize int) dto.ArtifactTransferOpenRequest {
|
||||
return dto.ArtifactTransferOpenRequest{
|
||||
RunEndpointID: "run-local",
|
||||
SessionToken: sessionToken,
|
||||
ArtifactID: "artifact-1",
|
||||
Direction: domain.ArtifactTransferDirectionUpload,
|
||||
OwnerKind: domain.ArtifactOwnerKindJob,
|
||||
OwnerID: "job-1",
|
||||
SizeBytes: int64(len(payload)),
|
||||
ChunkSizeBytes: chunkSize,
|
||||
Checksum: validator.BytesChecksum(payload),
|
||||
IdempotencyKey: "artifact-upload-1",
|
||||
}
|
||||
}
|
||||
|
||||
func validArtifactChunkRequest(sessionToken string, transferID string, payload []byte, index int, chunkSize int) dto.ArtifactChunkUploadRequest {
|
||||
offset := index * chunkSize
|
||||
end := offset + chunkSize
|
||||
if end > len(payload) {
|
||||
end = len(payload)
|
||||
}
|
||||
part := payload[offset:end]
|
||||
return dto.ArtifactChunkUploadRequest{
|
||||
RunEndpointID: "run-local",
|
||||
SessionToken: sessionToken,
|
||||
TransferID: transferID,
|
||||
ArtifactID: "artifact-1",
|
||||
ChunkIndex: index,
|
||||
Offset: int64(offset),
|
||||
SizeBytes: len(part),
|
||||
Checksum: validator.BytesChecksum(part),
|
||||
Payload: part,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user