Skip to content
Closed
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
2 changes: 2 additions & 0 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import (
bedrockbackend "github.com/blackbirdworks/gopherstack/services/bedrock"
bedrockruntimebackend "github.com/blackbirdworks/gopherstack/services/bedrockruntime"
cebackend "github.com/blackbirdworks/gopherstack/services/ce"
cleanroomsbackend "github.com/blackbirdworks/gopherstack/services/cleanrooms"
cloudcontrolbackend "github.com/blackbirdworks/gopherstack/services/cloudcontrol"
cfnbackend "github.com/blackbirdworks/gopherstack/services/cloudformation"
cloudfrontbackend "github.com/blackbirdworks/gopherstack/services/cloudfront"
Expand Down Expand Up @@ -2758,6 +2759,7 @@ func getMostRecentServiceProviders() []service.Provider {
&xraybackend.Provider{},
&s3tablesbackend.Provider{},
&databrewbackend.Provider{},
&cleanroomsbackend.Provider{},
&forecastbackend.Provider{},
&macie2backend.Provider{},
&appmeshbackend.Provider{},
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ require github.com/aws/aws-sdk-go-v2/service/networkmonitor v1.14.6

require github.com/aws/aws-sdk-go-v2/service/omics v1.45.0

require github.com/aws/aws-sdk-go-v2/service/cleanrooms v1.45.6

require (
github.com/antlr/antlr4 v0.0.0-20181218183524-be58ebffde8e // indirect
github.com/aws/aws-dax-go v1.2.15
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ github.com/aws/aws-sdk-go-v2/service/bedrockagent v1.54.0 h1:OnHTo0dbX2kWlAYHQZc
github.com/aws/aws-sdk-go-v2/service/bedrockagent v1.54.0/go.mod h1:zue4MN4ji6nlKYQYwVLmaPXJ66wB9JnIePX1e1yg5MU=
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.1 h1:tnLUbtNW5c056BEbQ4xvlZaakvgdaEdiKF87R1fxuoo=
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.1/go.mod h1:DYDD64rVUpCvpLyuWCiTaaSfrW2O9GiDo8S6fNo8ZI0=
github.com/aws/aws-sdk-go-v2/service/cleanrooms v1.45.6 h1:bxQlOwnJeYYz6P0ghQkPyrN1Kd5N02LbA6pEPhYw31U=
github.com/aws/aws-sdk-go-v2/service/cleanrooms v1.45.6/go.mod h1:fz3Qwhfu3co4zcOyQoTbqS2isrZviHAhi0ml0xoUpEU=
github.com/aws/aws-sdk-go-v2/service/cloudcontrol v1.29.15 h1:E3HjmGRKmA5R7YUzdidZWuxOSKqW95tZZlZ06wND9a0=
github.com/aws/aws-sdk-go-v2/service/cloudcontrol v1.29.15/go.mod h1:qdsQO5+urrlkcsolFWgiNQ0lpFB0UCQbTKK9j79b1Wg=
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.71.7 h1:QkM9aGnVnXrXpxXJMu7GO+E/eho+RfItwDp71aPa79o=
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2,776 changes: 2,776 additions & 0 deletions services/cleanrooms/backend.go

Large diffs are not rendered by default.

3,105 changes: 3,105 additions & 0 deletions services/cleanrooms/handler.go

Large diffs are not rendered by default.

285 changes: 285 additions & 0 deletions services/cleanrooms/handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
package cleanrooms_test

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/labstack/echo/v5"

"github.com/blackbirdworks/gopherstack/services/cleanrooms"
)

func newTestServer(t *testing.T) *echo.Echo {
t.Helper()
backend := cleanrooms.NewInMemoryBackend("123456789012", "us-east-1")
h := cleanrooms.NewHandler(backend)
e := echo.New()
e.Any("/*", h.Handler())

return e
}

func doRequest(
t *testing.T,
e *echo.Echo,
method, path string,
body any,
) *httptest.ResponseRecorder {
t.Helper()
var reqBody []byte
if body != nil {
var err error
reqBody, err = json.Marshal(body)
if err != nil {
t.Fatalf("marshal request: %v", err)
}
}
req := httptest.NewRequest(method, path, bytes.NewReader(reqBody))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

return rec
}

func TestCollaborationCRUD(t *testing.T) {
t.Parallel()

e := newTestServer(t)

createBody := map[string]any{
"name": "test-collab",
"description": "desc",
"creatorDisplayName": "Alice",
"creatorMemberAbilities": []string{"CAN_QUERY"},
"members": []any{},
"queryLogStatus": "ENABLED",
}

// Create collaboration
rec := doRequest(t, e, http.MethodPost, "/collaborations", createBody)
if rec.Code != http.StatusOK {
t.Fatalf("create: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
var createResp map[string]any
if err := json.NewDecoder(rec.Body).Decode(&createResp); err != nil {
t.Fatalf("decode create: %v", err)
}
if _, ok := createResp["collaboration"]; !ok {
t.Fatalf("missing key %q in response: %v", "collaboration", createResp)
}
collabID := createResp["collaboration"].(map[string]any)["collaborationIdentifier"].(string)

// List collaborations
rec = doRequest(t, e, http.MethodGet, "/collaborations", nil)
if rec.Code != http.StatusOK {
t.Fatalf("list: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
var listResp map[string]any
if err := json.NewDecoder(rec.Body).Decode(&listResp); err != nil {
t.Fatalf("decode list: %v", err)
}
if _, ok := listResp["collaborationList"]; !ok {
t.Fatalf("missing key %q in response: %v", "collaborationList", listResp)
}

// Get collaboration
rec = doRequest(t, e, http.MethodGet, "/collaborations/"+collabID, nil)
if rec.Code != http.StatusOK {
t.Fatalf("get: status %d: %s", rec.Code, rec.Body.String())
}

// Delete collaboration
rec = doRequest(t, e, http.MethodDelete, "/collaborations/"+collabID, nil)
if rec.Code != http.StatusOK {
t.Fatalf("delete: status %d: %s", rec.Code, rec.Body.String())
}

// Get deleted collaboration returns 404
rec = doRequest(t, e, http.MethodGet, "/collaborations/"+collabID, nil)
if rec.Code != http.StatusNotFound {
t.Fatalf("get deleted: status %d want 404: %s", rec.Code, rec.Body.String())
}
}

func TestConfiguredTableCRUD(t *testing.T) {
t.Parallel()

e := newTestServer(t)

createBody := map[string]any{
"name": "my-table",
"description": "desc",
"tableReference": map[string]any{
"glue": map[string]any{"databaseName": "db", "tableName": "tbl"},
},
"allowedColumns": []string{"col1"},
"analysisMethod": "DIRECT_QUERY",
}

// Create configured table
rec := doRequest(t, e, http.MethodPost, "/configuredTables", createBody)
if rec.Code != http.StatusOK {
t.Fatalf("create: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
var createResp map[string]any
if err := json.NewDecoder(rec.Body).Decode(&createResp); err != nil {
t.Fatalf("decode: %v", err)
}
ctID := createResp["configuredTable"].(map[string]any)["configuredTableIdentifier"].(string)

// List configured tables
rec = doRequest(t, e, http.MethodGet, "/configuredTables", nil)
if rec.Code != http.StatusOK {
t.Fatalf("list: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}

// Update configured table
rec = doRequest(
t,
e,
http.MethodPatch,
"/configuredTables/"+ctID,
map[string]any{"name": "new-name"},
)
if rec.Code != http.StatusOK {
t.Fatalf("update: status %d: %s", rec.Code, rec.Body.String())
}

// Delete configured table
rec = doRequest(t, e, http.MethodDelete, "/configuredTables/"+ctID, nil)
if rec.Code != http.StatusOK {
t.Fatalf("delete: status %d: %s", rec.Code, rec.Body.String())
}
}

func TestMembershipCRUD(t *testing.T) {
t.Parallel()

e := newTestServer(t)

colRec := doRequest(t, e, http.MethodPost, "/collaborations", map[string]any{
"name": "c1",
"description": "d",
"creatorDisplayName": "Bob",
"creatorMemberAbilities": []string{},
"members": []any{},
"queryLogStatus": "DISABLED",
})
if colRec.Code != http.StatusOK {
t.Fatalf("create collab: %s", colRec.Body.String())
}
var colResp map[string]any
_ = json.NewDecoder(colRec.Body).Decode(&colResp)
colID := colResp["collaboration"].(map[string]any)["collaborationIdentifier"].(string)

createBody := map[string]any{
"collaborationIdentifier": colID,
"queryLogStatus": "DISABLED",
}

// Create membership
rec := doRequest(t, e, http.MethodPost, "/memberships", createBody)
if rec.Code != http.StatusOK {
t.Fatalf("create: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
var createResp map[string]any
_ = json.NewDecoder(rec.Body).Decode(&createResp)
mID := createResp["membership"].(map[string]any)["membershipIdentifier"].(string)

// List memberships
rec = doRequest(t, e, http.MethodGet, "/memberships", nil)
if rec.Code != http.StatusOK {
t.Fatalf("list: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}

// Get membership
rec = doRequest(t, e, http.MethodGet, "/memberships/"+mID, nil)
if rec.Code != http.StatusOK {
t.Fatalf("get: status %d: %s", rec.Code, rec.Body.String())
}

// Delete membership
rec = doRequest(t, e, http.MethodDelete, "/memberships/"+mID, nil)
if rec.Code != http.StatusOK {
t.Fatalf("delete: status %d: %s", rec.Code, rec.Body.String())
}
}

func TestTagOperations(t *testing.T) {
t.Parallel()

e := newTestServer(t)

const testARN = "arn:aws:cleanrooms:us-east-1:123456789012:collaboration/abc123"

// Tag resource
rec := doRequest(
t,
e,
http.MethodPost,
"/tags/"+testARN,
map[string]any{"tags": map[string]string{"env": "test"}},
)
if rec.Code != http.StatusOK {
t.Fatalf("tag: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}

// List tags
rec = doRequest(t, e, http.MethodGet, "/tags/"+testARN, nil)
if rec.Code != http.StatusOK {
t.Fatalf("list tags: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}

// Untag resource
rec = doRequest(t, e, http.MethodDelete, "/tags/"+testARN+"?tagKeys=env", nil)
if rec.Code != http.StatusOK {
t.Fatalf("untag: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
}

func TestProtectedQueryLifecycle(t *testing.T) {
t.Parallel()

e := newTestServer(t)

// Create collaboration
colRec := doRequest(t, e, http.MethodPost, "/collaborations", map[string]any{
"name": "c2", "description": "d", "creatorDisplayName": "Carol",
"creatorMemberAbilities": []string{}, "members": []any{}, "queryLogStatus": "DISABLED",
})
var colResp map[string]any
_ = json.NewDecoder(colRec.Body).Decode(&colResp)
colID := colResp["collaboration"].(map[string]any)["collaborationIdentifier"].(string)

// Create membership
memRec := doRequest(t, e, http.MethodPost, "/memberships",
map[string]any{"collaborationIdentifier": colID, "queryLogStatus": "DISABLED"})
var memResp map[string]any
_ = json.NewDecoder(memRec.Body).Decode(&memResp)
mID := memResp["membership"].(map[string]any)["membershipIdentifier"].(string)

// Start protected query
rec := doRequest(t, e, http.MethodPost, "/memberships/"+mID+"/protectedQueries",
map[string]any{
"sqlParameters": map[string]any{"queryString": "SELECT 1"},
"resultConfiguration": map[string]any{},
})
if rec.Code != http.StatusOK {
t.Fatalf("start query: status %d: %s", rec.Code, rec.Body.String())
}
var resp map[string]any
_ = json.NewDecoder(rec.Body).Decode(&resp)
if _, ok := resp["protectedQuery"]; !ok {
t.Fatal("missing protectedQuery in response")
}

// List protected queries
rec = doRequest(t, e, http.MethodGet, "/memberships/"+mID+"/protectedQueries", nil)
if rec.Code != http.StatusOK {
t.Fatalf("list queries: status %d: %s", rec.Code, rec.Body.String())
}
}
Loading
Loading