Skip to content
Open
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
51 changes: 39 additions & 12 deletions internal/api/contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,33 @@ func assertErrorCode(t *testing.T, rec *httptest.ResponseRecorder, code string)
}
}

func decodeCreatedSubscriptionItems(t *testing.T, rec *httptest.ResponseRecorder) []map[string]any {
t.Helper()
body := decodeJSONMap(t, rec)
rawItems, ok := body["items"].([]any)
if !ok {
t.Fatalf("create subscription response items type: got %T, body=%s", body["items"], rec.Body.String())
}
items := make([]map[string]any, 0, len(rawItems))
for i := range rawItems {
item, ok := rawItems[i].(map[string]any)
if !ok {
t.Fatalf("create subscription response items[%d] type: got %T", i, rawItems[i])
}
items = append(items, item)
}
return items
}

func decodeCreatedSubscriptionFirstItem(t *testing.T, rec *httptest.ResponseRecorder) map[string]any {
t.Helper()
items := decodeCreatedSubscriptionItems(t, rec)
if len(items) == 0 {
t.Fatalf("create subscription response items is empty, body=%s", rec.Body.String())
}
return items[0]
}

func mustCreatePlatform(t *testing.T, srv *Server, name string) string {
t.Helper()

Expand Down Expand Up @@ -803,14 +830,14 @@ func TestAPIContract_KeywordFilteringOnListEndpoints(t *testing.T) {

rec = doJSONRequest(t, srv, http.MethodPost, "/api/v1/subscriptions", map[string]any{
"name": "Apple Feed",
"url": "https://example.com/apple",
"urls": []string{"https://example.com/apple"},
}, true)
if rec.Code != http.StatusCreated {
t.Fatalf("create subscription apple status: got %d, want %d, body=%s", rec.Code, http.StatusCreated, rec.Body.String())
}
rec = doJSONRequest(t, srv, http.MethodPost, "/api/v1/subscriptions", map[string]any{
"name": "Banana Feed",
"url": "https://example.com/banana",
"urls": []string{"https://example.com/banana"},
}, true)
if rec.Code != http.StatusCreated {
t.Fatalf("create subscription banana status: got %d, want %d, body=%s", rec.Code, http.StatusCreated, rec.Body.String())
Expand Down Expand Up @@ -1179,7 +1206,7 @@ func TestAPIContract_ModuleAndActionEndpoints(t *testing.T) {
// subscriptions
rec = doJSONRequest(t, srv, http.MethodPost, "/api/v1/subscriptions", map[string]any{
"name": "sub-a",
"url": "https://example.com/sub",
"urls": []string{"https://example.com/sub"},
}, true)
if rec.Code != http.StatusCreated {
t.Fatalf("create subscription status: got %d, want %d, body=%s", rec.Code, http.StatusCreated, rec.Body.String())
Expand Down Expand Up @@ -1319,7 +1346,7 @@ func TestAPIContract_SubscriptionUpdateIntervalMinimum(t *testing.T) {

rec := doJSONRequest(t, srv, http.MethodPost, "/api/v1/subscriptions", map[string]any{
"name": "too-fast",
"url": "https://example.com/sub-fast",
"urls": []string{"https://example.com/sub-fast"},
"update_interval": "10s",
}, true)
if rec.Code != http.StatusBadRequest {
Expand All @@ -1329,12 +1356,12 @@ func TestAPIContract_SubscriptionUpdateIntervalMinimum(t *testing.T) {

rec = doJSONRequest(t, srv, http.MethodPost, "/api/v1/subscriptions", map[string]any{
"name": "normal-sub",
"url": "https://example.com/sub-normal",
"urls": []string{"https://example.com/sub-normal"},
}, true)
if rec.Code != http.StatusCreated {
t.Fatalf("create subscription status: got %d, want %d, body=%s", rec.Code, http.StatusCreated, rec.Body.String())
}
body := decodeJSONMap(t, rec)
body := decodeCreatedSubscriptionFirstItem(t, rec)
subID, _ := body["id"].(string)
if subID == "" {
t.Fatalf("create subscription missing id: body=%s", rec.Body.String())
Expand All @@ -1354,12 +1381,12 @@ func TestAPIContract_SubscriptionEphemeralEvictDelay_DefaultAndCustom(t *testing

defaultRec := doJSONRequest(t, srv, http.MethodPost, "/api/v1/subscriptions", map[string]any{
"name": "default-evict-delay-sub",
"url": "https://example.com/sub-default-evict-delay",
"urls": []string{"https://example.com/sub-default-evict-delay"},
}, true)
if defaultRec.Code != http.StatusCreated {
t.Fatalf("create default delay subscription status: got %d, want %d, body=%s", defaultRec.Code, http.StatusCreated, defaultRec.Body.String())
}
defaultBody := decodeJSONMap(t, defaultRec)
defaultBody := decodeCreatedSubscriptionFirstItem(t, defaultRec)
if defaultBody["ephemeral_node_evict_delay"] != "72h0m0s" {
t.Fatalf(
"default ephemeral_node_evict_delay: got %v, want %q",
Expand All @@ -1370,13 +1397,13 @@ func TestAPIContract_SubscriptionEphemeralEvictDelay_DefaultAndCustom(t *testing

customRec := doJSONRequest(t, srv, http.MethodPost, "/api/v1/subscriptions", map[string]any{
"name": "custom-evict-delay-sub",
"url": "https://example.com/sub-custom-evict-delay",
"urls": []string{"https://example.com/sub-custom-evict-delay"},
"ephemeral_node_evict_delay": "30m",
}, true)
if customRec.Code != http.StatusCreated {
t.Fatalf("create custom delay subscription status: got %d, want %d, body=%s", customRec.Code, http.StatusCreated, customRec.Body.String())
}
customBody := decodeJSONMap(t, customRec)
customBody := decodeCreatedSubscriptionFirstItem(t, customRec)
if customBody["ephemeral_node_evict_delay"] != "30m0s" {
t.Fatalf(
"custom ephemeral_node_evict_delay: got %v, want %q",
Expand All @@ -1391,12 +1418,12 @@ func TestAPIContract_SubscriptionEphemeralEvictDelayPatchValidation(t *testing.T

createRec := doJSONRequest(t, srv, http.MethodPost, "/api/v1/subscriptions", map[string]any{
"name": "patch-evict-delay-sub",
"url": "https://example.com/sub-patch-evict-delay",
"urls": []string{"https://example.com/sub-patch-evict-delay"},
}, true)
if createRec.Code != http.StatusCreated {
t.Fatalf("create subscription status: got %d, want %d, body=%s", createRec.Code, http.StatusCreated, createRec.Body.String())
}
createBody := decodeJSONMap(t, createRec)
createBody := decodeCreatedSubscriptionFirstItem(t, createRec)
subID, _ := createBody["id"].(string)
if subID == "" {
t.Fatalf("create subscription missing id: body=%s", createRec.Body.String())
Expand Down
4 changes: 2 additions & 2 deletions internal/api/major_flow_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,12 @@ func TestMajorFlow_E2E_LocalProxyAndSubscriptionProvider(t *testing.T) {

createSubRec := doJSONRequest(t, h.apiServer, http.MethodPost, "/api/v1/subscriptions", map[string]any{
"name": "sub-major",
"url": subSource.URL + "/sub",
"urls": []string{subSource.URL + "/sub"},
}, true)
if createSubRec.Code != http.StatusCreated {
t.Fatalf("create subscription status: got %d, want %d, body=%s", createSubRec.Code, http.StatusCreated, createSubRec.Body.String())
}
subBody := decodeJSONMap(t, createSubRec)
subBody := decodeCreatedSubscriptionFirstItem(t, createSubRec)
subID, _ := subBody["id"].(string)
if subID == "" {
t.Fatalf("create subscription missing id: body=%s", createSubRec.Body.String())
Expand Down
116 changes: 116 additions & 0 deletions internal/api/subscription_batch_create_contract_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package api

import (
"net/http"
"strings"
"testing"
)

func TestAPIContract_SubscriptionCreateRemote_UsesURLsArrayAndKeepsBaseNameForSingle(t *testing.T) {
srv, _, _ := newControlPlaneTestServer(t)

rec := doJSONRequest(t, srv, http.MethodPost, "/api/v1/subscriptions", map[string]any{
"name": "single-sub",
"urls": []string{"https://api.example.com/sub"},
}, true)
if rec.Code != http.StatusCreated {
t.Fatalf("create single remote by urls status: got %d, want %d, body=%s", rec.Code, http.StatusCreated, rec.Body.String())
}

body := decodeJSONMap(t, rec)
itemsRaw, ok := body["items"].([]any)
if !ok {
t.Fatalf("create response items type: got %T, body=%s", body["items"], rec.Body.String())
}
if len(itemsRaw) != 1 {
t.Fatalf("create response items len: got %d, want %d, body=%s", len(itemsRaw), 1, rec.Body.String())
}
item, ok := itemsRaw[0].(map[string]any)
if !ok {
t.Fatalf("create response item type: got %T", itemsRaw[0])
}
if got, _ := item["name"].(string); got != "single-sub" {
t.Fatalf("single create name: got %q, want %q", got, "single-sub")
}
if got, _ := item["url"].(string); got != "https://api.example.com/sub" {
t.Fatalf("single create url: got %q, want %q", got, "https://api.example.com/sub")
}
}

func TestAPIContract_SubscriptionCreateRemote_MultipleURLsAppendDomainSuffix(t *testing.T) {
srv, _, _ := newControlPlaneTestServer(t)

rec := doJSONRequest(t, srv, http.MethodPost, "/api/v1/subscriptions", map[string]any{
"name": "batch-sub",
"urls": []string{
"https://a.example.com/sub-1",
"https://api.google.co.uk/sub-2",
"https://b.example.com/sub-3",
},
}, true)
if rec.Code != http.StatusCreated {
t.Fatalf("create multiple remote by urls status: got %d, want %d, body=%s", rec.Code, http.StatusCreated, rec.Body.String())
}

body := decodeJSONMap(t, rec)
itemsRaw, ok := body["items"].([]any)
if !ok {
t.Fatalf("create response items type: got %T, body=%s", body["items"], rec.Body.String())
}
if len(itemsRaw) != 3 {
t.Fatalf("create response items len: got %d, want %d, body=%s", len(itemsRaw), 3, rec.Body.String())
}

wantNames := []string{"batch-sub-example.com", "batch-sub-google.co.uk", "batch-sub-example.com-2"}
for i := range itemsRaw {
item, ok := itemsRaw[i].(map[string]any)
if !ok {
t.Fatalf("create response items[%d] type: got %T", i, itemsRaw[i])
}
if got, _ := item["name"].(string); got != wantNames[i] {
t.Fatalf("create response items[%d].name: got %q, want %q", i, got, wantNames[i])
}
}
}

func TestAPIContract_SubscriptionCreateRemote_URLFieldRejectedAndInvalidBatchIsAtomic(t *testing.T) {
srv, _, _ := newControlPlaneTestServer(t)

urlFieldRec := doJSONRequest(t, srv, http.MethodPost, "/api/v1/subscriptions", map[string]any{
"name": "legacy-url",
"url": "https://example.com/sub",
}, true)
if urlFieldRec.Code != http.StatusBadRequest {
t.Fatalf("create remote with url field status: got %d, want %d, body=%s", urlFieldRec.Code, http.StatusBadRequest, urlFieldRec.Body.String())
}
assertErrorCode(t, urlFieldRec, "INVALID_ARGUMENT")

invalidBatchRec := doJSONRequest(t, srv, http.MethodPost, "/api/v1/subscriptions", map[string]any{
"name": "atomic-batch",
"urls": []string{
"https://ok.example.com/sub-1",
"not-a-url",
"https://ok2.example.com/sub-2",
},
}, true)
if invalidBatchRec.Code != http.StatusBadRequest {
t.Fatalf("create invalid batch status: got %d, want %d, body=%s", invalidBatchRec.Code, http.StatusBadRequest, invalidBatchRec.Body.String())
}
assertErrorCode(t, invalidBatchRec, "INVALID_ARGUMENT")
if !strings.Contains(invalidBatchRec.Body.String(), "urls[1]") {
t.Fatalf("invalid batch error should contain urls index, body=%s", invalidBatchRec.Body.String())
}

listRec := doJSONRequest(t, srv, http.MethodGet, "/api/v1/subscriptions", nil, true)
if listRec.Code != http.StatusOK {
t.Fatalf("list subscriptions status: got %d, want %d, body=%s", listRec.Code, http.StatusOK, listRec.Body.String())
}
listBody := decodeJSONMap(t, listRec)
itemsRaw, ok := listBody["items"].([]any)
if !ok {
t.Fatalf("list items type: got %T, body=%s", listBody["items"], listRec.Body.String())
}
if len(itemsRaw) != 0 {
t.Fatalf("invalid batch should be atomic and create nothing, got items=%d body=%s", len(itemsRaw), listRec.Body.String())
}
}
4 changes: 2 additions & 2 deletions internal/api/subscription_cleanup_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ func TestAPIContract_SubscriptionCleanupAction_E2E(t *testing.T) {

createRec := doJSONRequest(t, srv, http.MethodPost, "/api/v1/subscriptions", map[string]any{
"name": "sub-cleanup-e2e",
"url": "https://example.com/sub",
"urls": []string{"https://example.com/sub"},
}, true)
if createRec.Code != http.StatusCreated {
t.Fatalf("create subscription status: got %d, want %d, body=%s", createRec.Code, http.StatusCreated, createRec.Body.String())
}
createBody := decodeJSONMap(t, createRec)
createBody := decodeCreatedSubscriptionFirstItem(t, createRec)
subID, _ := createBody["id"].(string)
if subID == "" {
t.Fatalf("create subscription missing id: body=%s", createRec.Body.String())
Expand Down
4 changes: 2 additions & 2 deletions internal/api/subscription_local_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ func TestAPIContract_SubscriptionSourceTypeReadOnlyOnPatch(t *testing.T) {

createRec := doJSONRequest(t, srv, http.MethodPost, "/api/v1/subscriptions", map[string]any{
"name": "sub-remote",
"url": "https://example.com/sub",
"urls": []string{"https://example.com/sub"},
}, true)
if createRec.Code != http.StatusCreated {
t.Fatalf("create remote subscription status: got %d, want %d, body=%s", createRec.Code, http.StatusCreated, createRec.Body.String())
}
body := decodeJSONMap(t, createRec)
body := decodeCreatedSubscriptionFirstItem(t, createRec)
subID, _ := body["id"].(string)
if subID == "" {
t.Fatalf("create remote subscription missing id: body=%s", createRec.Body.String())
Expand Down
6 changes: 3 additions & 3 deletions internal/api/subscription_refresh_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ func TestAPIContract_SubscriptionRefreshAction_E2EHTTPSource(t *testing.T) {

createRec := doJSONRequest(t, srv, http.MethodPost, "/api/v1/subscriptions", map[string]any{
"name": "sub-e2e",
"url": subscriptionSource.URL + "/sub",
"urls": []string{subscriptionSource.URL + "/sub"},
}, true)
if createRec.Code != http.StatusCreated {
t.Fatalf("create subscription status: got %d, want %d, body=%s", createRec.Code, http.StatusCreated, createRec.Body.String())
}
createBody := decodeJSONMap(t, createRec)
createBody := decodeCreatedSubscriptionFirstItem(t, createRec)
subID, _ := createBody["id"].(string)
if subID == "" {
t.Fatalf("create subscription missing id: body=%s", createRec.Body.String())
Expand Down Expand Up @@ -146,7 +146,7 @@ func TestAPIContract_SubscriptionRefreshAction_E2ELocalSource(t *testing.T) {
if createRec.Code != http.StatusCreated {
t.Fatalf("create local subscription status: got %d, want %d, body=%s", createRec.Code, http.StatusCreated, createRec.Body.String())
}
createBody := decodeJSONMap(t, createRec)
createBody := decodeCreatedSubscriptionFirstItem(t, createRec)
subID, _ := createBody["id"].(string)
if subID == "" {
t.Fatalf("create local subscription missing id: body=%s", createRec.Body.String())
Expand Down
Loading