162 lines
6.9 KiB
Go
162 lines
6.9 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"browser.local/run/protocol"
|
|
)
|
|
|
|
func TestPlatformClientArtifactMethodsUploadRawChunksAndDecodeResponses(t *testing.T) {
|
|
seen := map[string]bool{}
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
seen[r.URL.Path] = true
|
|
switch r.URL.Path {
|
|
case "/api/v1/run/artifacts/open":
|
|
var request protocol.ArtifactTransferOpenRequest
|
|
decodeTestRequest(t, r, &request)
|
|
if request.ArtifactID != "artifact-1" || request.ChunkSizeBytes != 8 {
|
|
t.Fatalf("unexpected artifact open request: %+v", request)
|
|
}
|
|
writeTestJSON(t, w, validArtifactOpenResponse())
|
|
case "/api/v1/run/artifacts/chunks":
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
t.Fatalf("read artifact chunk body: %v", err)
|
|
}
|
|
verifyRunRequestSignature(t, r, body, "run-local", "session-token")
|
|
if r.Header.Get("Content-Type") != "application/octet-stream" || r.Header.Get("X-Run-Session-Token") != "session-token" {
|
|
t.Fatalf("unexpected artifact chunk content headers: %+v", r.Header)
|
|
}
|
|
if r.Header.Get("X-Artifact-Transfer-Id") != "transfer-1" || r.Header.Get("X-Artifact-Chunk-Index") != "0" || string(body) != "payload" {
|
|
t.Fatalf("unexpected artifact chunk request headers=%+v body=%q", r.Header, string(body))
|
|
}
|
|
writeTestJSON(t, w, protocol.ArtifactChunkUploadResponse{Accepted: true, TransferID: "transfer-1", ArtifactID: "artifact-1", ChunkIndex: 0, ReceivedChunkIndexes: []int{0}, NextMissingChunkIndex: 1, ServerTime: fixedClientTestTime()})
|
|
case "/api/v1/run/artifacts/status":
|
|
var request protocol.ArtifactTransferStatusRequest
|
|
decodeTestRequest(t, r, &request)
|
|
if request.TransferID != "transfer-1" {
|
|
t.Fatalf("unexpected artifact status request: %+v", request)
|
|
}
|
|
writeTestJSON(t, w, protocol.ArtifactTransferStatusResponse{Accepted: true, TransferID: "transfer-1", ArtifactID: "artifact-1", Direction: "upload", TotalChunks: 2, ChunkSizeBytes: 8, ReceivedChunkIndexes: []int{0}, NextMissingChunkIndex: 1, ServerTime: fixedClientTestTime()})
|
|
case "/api/v1/run/artifacts/complete":
|
|
var request protocol.ArtifactTransferCompleteRequest
|
|
decodeTestRequest(t, r, &request)
|
|
if request.Checksum == "" || request.SizeBytes != 7 {
|
|
t.Fatalf("unexpected artifact complete request: %+v", request)
|
|
}
|
|
response := protocol.ArtifactTransferCompleteResponse{Accepted: true, TransferID: "transfer-1", Artifact: validArtifactMetadata("available"), Completed: true, ServerTime: fixedClientTestTime()}
|
|
writeTestJSON(t, w, response)
|
|
default:
|
|
t.Fatalf("unexpected request path %s", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
client, err := NewPlatformClient(server.URL)
|
|
if err != nil {
|
|
t.Fatalf("new client: %v", err)
|
|
}
|
|
ctx := context.Background()
|
|
open, err := client.OpenArtifactTransfer(ctx, validClientArtifactOpen())
|
|
if err != nil || !open.Accepted || open.TransferID != "transfer-1" {
|
|
t.Fatalf("open artifact response=%+v err=%v", open, err)
|
|
}
|
|
chunk, err := client.UploadArtifactChunk(ctx, validClientArtifactChunk())
|
|
if err != nil || chunk.NextMissingChunkIndex != 1 {
|
|
t.Fatalf("upload artifact chunk response=%+v err=%v", chunk, err)
|
|
}
|
|
status, err := client.QueryArtifactTransferStatus(ctx, validClientArtifactStatus())
|
|
if err != nil || len(status.ReceivedChunkIndexes) != 1 {
|
|
t.Fatalf("artifact status response=%+v err=%v", status, err)
|
|
}
|
|
complete, err := client.CompleteArtifactTransfer(ctx, validClientArtifactComplete())
|
|
if err != nil || !complete.Completed || complete.Artifact.State != "available" {
|
|
t.Fatalf("complete artifact response=%+v err=%v", complete, err)
|
|
}
|
|
|
|
for _, path := range []string{"/api/v1/run/artifacts/open", "/api/v1/run/artifacts/chunks", "/api/v1/run/artifacts/status", "/api/v1/run/artifacts/complete"} {
|
|
if !seen[path] {
|
|
t.Fatalf("expected request to %s", path)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPlatformClientArtifactMethodReturnsErrorForPlatformFailure(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_, _ = w.Write([]byte(`{"code":"validation_failed"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client, err := NewPlatformClient(server.URL)
|
|
if err != nil {
|
|
t.Fatalf("new client: %v", err)
|
|
}
|
|
if _, err := client.OpenArtifactTransfer(context.Background(), validClientArtifactOpen()); err == nil {
|
|
t.Fatal("expected platform error")
|
|
}
|
|
}
|
|
|
|
func TestArtifactChunkPayloadIsNotJSONEncoded(t *testing.T) {
|
|
encoded, err := json.Marshal(validClientArtifactChunk())
|
|
if err != nil {
|
|
t.Fatalf("marshal artifact chunk: %v", err)
|
|
}
|
|
if !json.Valid(encoded) || containsJSONPayloadField(encoded) {
|
|
t.Fatalf("artifact chunk payload must stay out of JSON, got %s", string(encoded))
|
|
}
|
|
}
|
|
|
|
func validClientArtifactOpen() protocol.ArtifactTransferOpenRequest {
|
|
return protocol.ArtifactTransferOpenRequest{
|
|
RunEndpointID: "run-local",
|
|
SessionToken: "session-token",
|
|
ArtifactID: "artifact-1",
|
|
Direction: "upload",
|
|
OwnerKind: "job",
|
|
OwnerID: "job-1",
|
|
SizeBytes: 7,
|
|
ChunkSizeBytes: 8,
|
|
Checksum: validSHA256Checksum(),
|
|
IdempotencyKey: "artifact-upload-1",
|
|
}
|
|
}
|
|
|
|
func validClientArtifactChunk() protocol.ArtifactChunkUploadRequest {
|
|
return protocol.ArtifactChunkUploadRequest{RunEndpointID: "run-local", SessionToken: "session-token", TransferID: "transfer-1", ArtifactID: "artifact-1", ChunkIndex: 0, Offset: 0, SizeBytes: 7, Checksum: validSHA256Checksum(), Payload: []byte("payload")}
|
|
}
|
|
|
|
func validClientArtifactStatus() protocol.ArtifactTransferStatusRequest {
|
|
return protocol.ArtifactTransferStatusRequest{RunEndpointID: "run-local", SessionToken: "session-token", TransferID: "transfer-1", ArtifactID: "artifact-1"}
|
|
}
|
|
|
|
func validClientArtifactComplete() protocol.ArtifactTransferCompleteRequest {
|
|
return protocol.ArtifactTransferCompleteRequest{RunEndpointID: "run-local", SessionToken: "session-token", TransferID: "transfer-1", ArtifactID: "artifact-1", Checksum: validSHA256Checksum(), SizeBytes: 7}
|
|
}
|
|
|
|
func validArtifactOpenResponse() protocol.ArtifactTransferOpenResponse {
|
|
return protocol.ArtifactTransferOpenResponse{Accepted: true, TransferID: "transfer-1", Direction: "upload", Artifact: validArtifactMetadata("uploading"), TotalChunks: 2, ChunkSizeBytes: 8, NextMissingChunkIndex: 0, ServerTime: fixedClientTestTime()}
|
|
}
|
|
|
|
func validArtifactMetadata(state string) protocol.ArtifactMetadata {
|
|
return protocol.ArtifactMetadata{ID: "artifact-1", OwnerKind: "job", OwnerID: "job-1", SizeBytes: 7, Checksum: validSHA256Checksum(), State: state, CreatedAt: fixedClientTestTime(), UpdatedAt: fixedClientTestTime()}
|
|
}
|
|
|
|
func validSHA256Checksum() string {
|
|
return "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
|
}
|
|
|
|
func containsJSONPayloadField(encoded []byte) bool {
|
|
var body map[string]any
|
|
if err := json.Unmarshal(encoded, &body); err != nil {
|
|
return false
|
|
}
|
|
_, exists := body["payload"]
|
|
return exists
|
|
}
|