NOISSUE - Fix client keys (#3539)
Property Based Tests / api-test (push) Has been cancelled
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled

Signed-off-by: Arvindh <arvindh91@gmail.com>
This commit is contained in:
Arvindh
2026-06-30 23:40:33 +05:30
committed by GitHub
parent ededf5308c
commit 3b5f646719
6 changed files with 403 additions and 85 deletions
+24
View File
@@ -193,6 +193,8 @@ jobs:
base-url: ${{ env.USERS_URL }} base-url: ${{ env.USERS_URL }}
checks: all checks: all
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --exclude-operation-id=requestPasswordReset --phases=examples' args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --exclude-operation-id=requestPasswordReset --phases=examples'
coverage-artifact-name: schema-coverage-users
coverage-pr-comment: false
- name: Run Groups API tests - name: Run Groups API tests
if: (steps.changes.outputs.groups == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('groups/api/http/**') != '' if: (steps.changes.outputs.groups == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('groups/api/http/**') != ''
@@ -202,6 +204,8 @@ jobs:
base-url: ${{ env.GROUPS_URL }} base-url: ${{ env.GROUPS_URL }}
checks: all checks: all
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples' args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
coverage-artifact-name: schema-coverage-groups
coverage-pr-comment: false
- name: Run Clients API tests - name: Run Clients API tests
if: (steps.changes.outputs.clients == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('clients/api/http/**') != '' if: (steps.changes.outputs.clients == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('clients/api/http/**') != ''
@@ -211,6 +215,8 @@ jobs:
base-url: ${{ env.CLIENTS_URL }} base-url: ${{ env.CLIENTS_URL }}
checks: all checks: all
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples' args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
coverage-artifact-name: schema-coverage-clients
coverage-pr-comment: false
- name: Run Channels API tests - name: Run Channels API tests
if: (steps.changes.outputs.channels == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('channels/api/http/**') != '' if: (steps.changes.outputs.channels == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('channels/api/http/**') != ''
@@ -220,6 +226,8 @@ jobs:
base-url: ${{ env.CHANNELS_URL }} base-url: ${{ env.CHANNELS_URL }}
checks: all checks: all
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples' args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
coverage-artifact-name: schema-coverage-channels
coverage-pr-comment: false
- name: Run Domains API tests - name: Run Domains API tests
if: (steps.changes.outputs.domains == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('domains/api/http/**') != '' if: (steps.changes.outputs.domains == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('domains/api/http/**') != ''
@@ -229,6 +237,8 @@ jobs:
base-url: ${{ env.DOMAIN_URL }} base-url: ${{ env.DOMAIN_URL }}
checks: all checks: all
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples' args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
coverage-artifact-name: schema-coverage-domains
coverage-pr-comment: false
- name: Run Journal API tests - name: Run Journal API tests
if: steps.changes.outputs.journal == 'true' || steps.changes.outputs.workflow == 'true' if: steps.changes.outputs.journal == 'true' || steps.changes.outputs.workflow == 'true'
@@ -238,6 +248,8 @@ jobs:
base-url: ${{ env.JOURNAL_URL }} base-url: ${{ env.JOURNAL_URL }}
checks: all checks: all
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples' args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
coverage-artifact-name: schema-coverage-journal
coverage-pr-comment: false
- name: Run Bootstrap API tests - name: Run Bootstrap API tests
if: (steps.changes.outputs.bootstrap == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('bootstrap/api/**') != '' if: (steps.changes.outputs.bootstrap == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('bootstrap/api/**') != ''
@@ -247,6 +259,8 @@ jobs:
base-url: ${{ env.BOOTSTRAP_URL }} base-url: ${{ env.BOOTSTRAP_URL }}
checks: all checks: all
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples' args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
coverage-artifact-name: schema-coverage-bootstrap
coverage-pr-comment: false
- name: Run Certs API tests - name: Run Certs API tests
if: (steps.changes.outputs.certs == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('docker/addons/certs/docker-compose.yaml') != '' if: (steps.changes.outputs.certs == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('docker/addons/certs/docker-compose.yaml') != ''
@@ -256,6 +270,8 @@ jobs:
base-url: ${{ env.CERTS_URL }} base-url: ${{ env.CERTS_URL }}
checks: all checks: all
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples' args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
coverage-artifact-name: schema-coverage-certs
coverage-pr-comment: false
- name: Run Readers API tests - name: Run Readers API tests
if: steps.changes.outputs.readers == 'true' || steps.changes.outputs.workflow == 'true' if: steps.changes.outputs.readers == 'true' || steps.changes.outputs.workflow == 'true'
@@ -265,6 +281,8 @@ jobs:
base-url: ${{ env.READERS_URL }} base-url: ${{ env.READERS_URL }}
checks: all checks: all
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples' args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
coverage-artifact-name: schema-coverage-readers
coverage-pr-comment: false
- name: Run Rules Engine API tests - name: Run Rules Engine API tests
if: steps.changes.outputs.re == 'true' || steps.changes.outputs.workflow == 'true' if: steps.changes.outputs.re == 'true' || steps.changes.outputs.workflow == 'true'
@@ -274,6 +292,8 @@ jobs:
base-url: ${{ env.RE_URL }} base-url: ${{ env.RE_URL }}
checks: all checks: all
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples' args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
coverage-artifact-name: schema-coverage-rules
coverage-pr-comment: false
- name: Run Alarms API tests - name: Run Alarms API tests
if: steps.changes.outputs.alarms == 'true' || steps.changes.outputs.workflow == 'true' if: steps.changes.outputs.alarms == 'true' || steps.changes.outputs.workflow == 'true'
@@ -283,6 +303,8 @@ jobs:
base-url: ${{ env.ALARMS_URL }} base-url: ${{ env.ALARMS_URL }}
checks: all checks: all
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples' args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
coverage-artifact-name: schema-coverage-alarms
coverage-pr-comment: false
- name: Run Reports API tests - name: Run Reports API tests
if: steps.changes.outputs.reports == 'true' || steps.changes.outputs.workflow == 'true' if: steps.changes.outputs.reports == 'true' || steps.changes.outputs.workflow == 'true'
@@ -292,6 +314,8 @@ jobs:
base-url: ${{ env.REPORTS_URL }} base-url: ${{ env.REPORTS_URL }}
checks: all checks: all
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples' args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
coverage-artifact-name: schema-coverage-reports
coverage-pr-comment: false
- name: Stop containers - name: Stop containers
if: always() if: always()
+1 -75
View File
@@ -9,7 +9,6 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
@@ -54,40 +53,6 @@ type config struct {
InstanceID string `env:"MG_FLUXMQ_INSTANCE_ID" envDefault:""` InstanceID string `env:"MG_FLUXMQ_INSTANCE_ID" envDefault:""`
} }
type fanoutPublisher struct {
publishers []messaging.Publisher
}
func (fp fanoutPublisher) Publish(ctx context.Context, topic string, msg *messaging.Message) error {
for _, publisher := range fp.publishers {
if err := publisher.Publish(ctx, topic, msg); err != nil {
return err
}
}
return nil
}
func (fp fanoutPublisher) Close() error {
errs := make([]error, 0, len(fp.publishers))
for _, publisher := range fp.publishers {
errs = append(errs, publisher.Close())
}
return errors.Join(errs...)
}
type writerBridgeHandler struct {
ctx context.Context
publisher messaging.Publisher
}
func (h writerBridgeHandler) Handle(msg *messaging.Message) error {
return h.publisher.Publish(h.ctx, messaging.EncodeMessageTopic(msg), msg)
}
func (h writerBridgeHandler) Cancel() error {
return nil
}
func main() { func main() {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
g, ctx := errgroup.WithContext(ctx) g, ctx := errgroup.WithContext(ctx)
@@ -200,45 +165,6 @@ func main() {
} }
defer messagePublisher.Close() defer messagePublisher.Close()
writerPublisher, err := fluxmqbroker.NewUndeclaredPublisher(
ctx,
cfg.BrokerURL,
fluxmqbroker.Prefix("writers"),
fluxmqbroker.ConnectionName("fluxmq-ui-publish-proxy"),
)
if err != nil {
logger.Error(fmt.Sprintf("failed to create publish proxy writer publisher: %s", err))
exitCode = 1
return
}
defer writerPublisher.Close()
publisher := fanoutPublisher{publishers: []messaging.Publisher{messagePublisher, writerPublisher}}
writerBridge, err := fluxmqbroker.NewPubSub(
ctx,
cfg.BrokerURL,
logger,
fluxmqbroker.DirectTopicOnly(),
fluxmqbroker.ConnectionName("fluxmq-mqtt-writer-bridge"),
)
if err != nil {
logger.Error(fmt.Sprintf("failed to create MQTT writer bridge subscriber: %s", err))
exitCode = 1
return
}
defer writerBridge.Close()
if err := writerBridge.Subscribe(ctx, messaging.SubscriberConfig{
ID: cfg.InstanceID + "-mqtt-writer-bridge",
Topic: "m/#",
Handler: writerBridgeHandler{ctx: ctx, publisher: writerPublisher},
DeliveryPolicy: messaging.DeliverNewPolicy,
}); err != nil {
logger.Error(fmt.Sprintf("failed to subscribe MQTT writer bridge: %s", err))
exitCode = 1
return
}
logger.Info("FluxMQ MQTT writer bridge subscribed", "topic", "m/#")
httpServerConfig := server.Config{Port: "9026"} httpServerConfig := server.Config{Port: "9026"}
if err := env.ParseWithOptions(&httpServerConfig, env.Options{Prefix: envPrefixHTTP}); err != nil { if err := env.ParseWithOptions(&httpServerConfig, env.Options{Prefix: envPrefixHTTP}); err != nil {
logger.Error(fmt.Sprintf("failed to load publish proxy HTTP server configuration: %s", err)) logger.Error(fmt.Sprintf("failed to load publish proxy HTTP server configuration: %s", err))
@@ -250,7 +176,7 @@ func main() {
cancel, cancel,
"fluxmq-publish", "fluxmq-publish",
httpServerConfig, httpServerConfig,
fluxmqhttp.MakePublishHandler(authn, atomAuthz, publisher), fluxmqhttp.MakePublishHandler(authn, atomAuthz, messagePublisher),
logger, logger,
) )
+68 -10
View File
@@ -56,7 +56,7 @@ func (c *Client) CreateTenant(ctx context.Context, tenant Tenant) (Tenant, error
CreateTenant Tenant `json:"createTenant"` CreateTenant Tenant `json:"createTenant"`
} }
err := c.graphQL(ctx, `mutation CreateTenant($input: CreateTenantInput!) { err := c.graphQL(ctx, `mutation CreateTenant($input: CreateTenantInput!) {
createTenant(input: $input) { id name route status tags attributes created_by: createdBy updated_by: updatedBy created_at: createdAt updated_at: updatedAt } createTenant(input: $input) { id name route: alias status tags attributes created_by: createdBy updated_by: updatedBy created_at: createdAt updated_at: updatedAt }
}`, map[string]any{"input": tenantCreateInput(tenant)}, &out) }`, map[string]any{"input": tenantCreateInput(tenant)}, &out)
return out.CreateTenant, err return out.CreateTenant, err
} }
@@ -66,7 +66,7 @@ func (c *Client) GetTenant(ctx context.Context, id string) (Tenant, error) {
Tenant Tenant `json:"tenant"` Tenant Tenant `json:"tenant"`
} }
err := c.graphQL(ctx, `query Tenant($id: ID!) { err := c.graphQL(ctx, `query Tenant($id: ID!) {
tenant(id: $id) { id name route status tags attributes created_by: createdBy updated_by: updatedBy created_at: createdAt updated_at: updatedAt } tenant(id: $id) { id name route: alias status tags attributes created_by: createdBy updated_by: updatedBy created_at: createdAt updated_at: updatedAt }
}`, map[string]any{"id": id}, &out) }`, map[string]any{"id": id}, &out)
return out.Tenant, err return out.Tenant, err
} }
@@ -76,7 +76,7 @@ func (c *Client) UpdateTenant(ctx context.Context, id string, tenant Tenant) (Te
UpdateTenant Tenant `json:"updateTenant"` UpdateTenant Tenant `json:"updateTenant"`
} }
err := c.graphQL(ctx, `mutation UpdateTenant($id: ID!, $input: UpdateTenantInput!) { err := c.graphQL(ctx, `mutation UpdateTenant($id: ID!, $input: UpdateTenantInput!) {
updateTenant(id: $id, input: $input) { id name route status tags attributes created_by: createdBy updated_by: updatedBy created_at: createdAt updated_at: updatedAt } updateTenant(id: $id, input: $input) { id name route: alias status tags attributes created_by: createdBy updated_by: updatedBy created_at: createdAt updated_at: updatedAt }
}`, map[string]any{"id": id, "input": tenantUpdateInput(tenant)}, &out) }`, map[string]any{"id": id, "input": tenantUpdateInput(tenant)}, &out)
return out.UpdateTenant, err return out.UpdateTenant, err
} }
@@ -92,7 +92,7 @@ func (c *Client) ChangeTenantStatus(ctx context.Context, id, action string) (Ten
} }
var out map[string]Tenant var out map[string]Tenant
err := c.graphQL(ctx, fmt.Sprintf(`mutation ChangeTenantStatus($id: ID!) { err := c.graphQL(ctx, fmt.Sprintf(`mutation ChangeTenantStatus($id: ID!) {
%s(id: $id) { id name route status tags attributes created_by: createdBy updated_by: updatedBy created_at: createdAt updated_at: updatedAt } %s(id: $id) { id name route: alias status tags attributes created_by: createdBy updated_by: updatedBy created_at: createdAt updated_at: updatedAt }
}`, field), map[string]any{"id": id}, &out) }`, field), map[string]any{"id": id}, &out)
if err != nil { if err != nil {
return Tenant{}, err return Tenant{}, err
@@ -234,10 +234,10 @@ func (c *Client) ListTenants(ctx context.Context, q Query) (TenantList, error) {
var out struct { var out struct {
Tenants TenantList `json:"tenants"` Tenants TenantList `json:"tenants"`
} }
err := c.graphQL(ctx, `query Tenants($q: String, $name: String, $route: String, $status: TenantStatus, $limit: Int, $offset: Int) { err := c.graphQL(ctx, `query Tenants($q: String, $name: String, $alias: String, $status: TenantStatus, $limit: Int, $offset: Int) {
tenants(q: $q, name: $name, route: $route, status: $status, limit: $limit, offset: $offset) { tenants(q: $q, name: $name, alias: $alias, status: $status, limit: $limit, offset: $offset) {
total total
items { id name route status tags attributes created_by: createdBy updated_by: updatedBy created_at: createdAt updated_at: updatedAt } items { id name route: alias status tags attributes created_by: createdBy updated_by: updatedBy created_at: createdAt updated_at: updatedAt }
} }
}`, queryVariables(q), &out) }`, queryVariables(q), &out)
return out.Tenants, err return out.Tenants, err
@@ -506,6 +506,64 @@ func (c *Client) CreateAPIKey(ctx context.Context, entityID, description string)
return out.CreateAPIKey, err return out.CreateAPIKey, err
} }
func (c *Client) CreateSharedKey(ctx context.Context, entityID, key, description string) (SharedKeyResponse, error) {
var out struct {
CreateSharedKey SharedKeyResponse `json:"createSharedKey"`
}
input := map[string]any{}
setIfNotEmpty(input, "key", key)
setIfNotEmpty(input, "description", description)
err := c.graphQL(ctx, `mutation CreateSharedKey($entityId: ID!, $input: CreateSharedKeyInput!) {
createSharedKey(entityId: $entityId, input: $input) {
credentialId
key
expiresAt
}
}`, map[string]any{
"entityId": entityID,
"input": input,
}, &out)
return out.CreateSharedKey, err
}
func (c *Client) RevealSharedKey(ctx context.Context, entityID, credentialID string) (SharedKeyResponse, error) {
var out struct {
RevealSharedKey SharedKeyResponse `json:"revealSharedKey"`
}
err := c.graphQL(ctx, `mutation RevealSharedKey($entityId: ID!, $credentialId: ID!) {
revealSharedKey(entityId: $entityId, credentialId: $credentialId) {
credentialId
key
expiresAt
}
}`, map[string]any{
"entityId": entityID,
"credentialId": credentialID,
}, &out)
return out.RevealSharedKey, err
}
func (c *Client) ListCredentials(ctx context.Context, entityID string) (CredentialList, error) {
var out struct {
Credentials CredentialList `json:"credentials"`
}
err := c.graphQL(ctx, `query Credentials($entityId: ID!) {
credentials(entityId: $entityId) {
total
items {
id
entity_id: entityId
kind
identifier
status
expires_at: expiresAt
created_at: createdAt
}
}
}`, map[string]any{"entityId": entityID}, &out)
return out.Credentials, err
}
func (c *Client) RevokeCredential(ctx context.Context, entityID, credentialID string) error { func (c *Client) RevokeCredential(ctx context.Context, entityID, credentialID string) error {
return c.graphQL(ctx, `mutation RevokeCredential($entityId: ID!, $credentialId: ID!) { return c.graphQL(ctx, `mutation RevokeCredential($entityId: ID!, $credentialId: ID!) {
revokeCredential(entityId: $entityId, credentialId: $credentialId) revokeCredential(entityId: $entityId, credentialId: $credentialId)
@@ -632,7 +690,7 @@ func graphQLErr(errors []graphQLErrorItem) error {
func tenantCreateInput(tenant Tenant) map[string]any { func tenantCreateInput(tenant Tenant) map[string]any {
input := map[string]any{"name": tenant.Name} input := map[string]any{"name": tenant.Name}
setIfNotEmpty(input, "id", tenant.ID) setIfNotEmpty(input, "id", tenant.ID)
setIfNotEmpty(input, "route", tenant.Route) setIfNotEmpty(input, "alias", tenant.Route)
if tenant.Tags != nil { if tenant.Tags != nil {
input["tags"] = tenant.Tags input["tags"] = tenant.Tags
} }
@@ -645,7 +703,7 @@ func tenantCreateInput(tenant Tenant) map[string]any {
func tenantUpdateInput(tenant Tenant) map[string]any { func tenantUpdateInput(tenant Tenant) map[string]any {
input := map[string]any{} input := map[string]any{}
setIfNotEmpty(input, "name", tenant.Name) setIfNotEmpty(input, "name", tenant.Name)
setIfNotEmpty(input, "route", tenant.Route) setIfNotEmpty(input, "alias", tenant.Route)
if tenant.Tags != nil { if tenant.Tags != nil {
input["tags"] = tenant.Tags input["tags"] = tenant.Tags
} }
@@ -798,7 +856,7 @@ func queryVariables(q Query) map[string]any {
vars := map[string]any{} vars := map[string]any{}
setIfNotEmpty(vars, "q", q.Q) setIfNotEmpty(vars, "q", q.Q)
setIfNotEmpty(vars, "name", q.Name) setIfNotEmpty(vars, "name", q.Name)
setIfNotEmpty(vars, "route", q.Route) setIfNotEmpty(vars, "alias", q.Route)
setIfNotEmpty(vars, "kind", q.Kind) setIfNotEmpty(vars, "kind", q.Kind)
setIfNotEmpty(vars, "tenantId", q.TenantID) setIfNotEmpty(vars, "tenantId", q.TenantID)
setIfNotEmpty(vars, "status", q.Status) setIfNotEmpty(vars, "status", q.Status)
+285
View File
@@ -94,6 +94,291 @@ func TestListResources(t *testing.T) {
} }
} }
func TestCreateTenantMapsRouteToAlias(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var payload struct {
Query string `json:"query"`
Variables map[string]any `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
if !strings.Contains(payload.Query, "createTenant") || !strings.Contains(payload.Query, "route: alias") {
t.Fatalf("query does not map tenant alias to route: %s", payload.Query)
}
input, ok := payload.Variables["input"].(map[string]any)
if !ok {
t.Fatalf("unexpected input: %+v", payload.Variables["input"])
}
if input["alias"] != "d1" {
t.Fatalf("expected alias input from route, got: %+v", input)
}
if _, ok := input["route"]; ok {
t.Fatalf("input must not use Atom route field: %+v", input)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"createTenant": map[string]any{
"id": testTenantID,
"name": "D1",
"route": "d1",
"status": "active",
},
},
})
}))
defer srv.Close()
client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
got, err := client.CreateTenant(context.Background(), Tenant{Name: "D1", Route: "d1"})
if err != nil {
t.Fatalf("create tenant failed: %v", err)
}
if got.ID != testTenantID || got.Route != "d1" {
t.Fatalf("unexpected tenant: %+v", got)
}
}
func TestUpdateTenantMapsRouteToAlias(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var payload struct {
Query string `json:"query"`
Variables map[string]any `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
if !strings.Contains(payload.Query, "updateTenant") || !strings.Contains(payload.Query, "route: alias") {
t.Fatalf("query does not map tenant alias to route: %s", payload.Query)
}
if payload.Variables["id"] != testTenantID {
t.Fatalf("unexpected variables: %+v", payload.Variables)
}
input, ok := payload.Variables["input"].(map[string]any)
if !ok {
t.Fatalf("unexpected input: %+v", payload.Variables["input"])
}
if input["alias"] != "d1" {
t.Fatalf("expected alias input from route, got: %+v", input)
}
if _, ok := input["route"]; ok {
t.Fatalf("input must not use Atom route field: %+v", input)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"updateTenant": map[string]any{
"id": testTenantID,
"name": "D1",
"route": "d1",
"status": "active",
},
},
})
}))
defer srv.Close()
client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
got, err := client.UpdateTenant(context.Background(), testTenantID, Tenant{Name: "D1", Route: "d1"})
if err != nil {
t.Fatalf("update tenant failed: %v", err)
}
if got.ID != testTenantID || got.Route != "d1" {
t.Fatalf("unexpected tenant: %+v", got)
}
}
func TestListTenantsMapsRouteToAlias(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var payload struct {
Query string `json:"query"`
Variables map[string]any `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
if !strings.Contains(payload.Query, "$alias: String") ||
!strings.Contains(payload.Query, "alias: $alias") ||
!strings.Contains(payload.Query, "route: alias") {
t.Fatalf("query does not use Atom alias for tenant route lookup: %s", payload.Query)
}
if strings.Contains(payload.Query, "$route") || strings.Contains(payload.Query, "route: $route") {
t.Fatalf("query must not use removed Atom route field/filter: %s", payload.Query)
}
if payload.Variables["alias"] != "d1" {
t.Fatalf("expected alias variable from route, got: %+v", payload.Variables)
}
if _, ok := payload.Variables["route"]; ok {
t.Fatalf("variables must not use Atom route field: %+v", payload.Variables)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"tenants": map[string]any{
"items": []Tenant{{ID: testTenantID, Name: "D1", Route: "d1", Status: "active"}},
"total": 1,
},
},
})
}))
defer srv.Close()
client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
got, err := client.ListTenants(context.Background(), Query{Route: "d1", Limit: 1})
if err != nil {
t.Fatalf("list tenants failed: %v", err)
}
if got.Total != 1 || len(got.Items) != 1 || got.Items[0].Route != "d1" {
t.Fatalf("unexpected tenants: %+v", got)
}
}
func TestCreateSharedKey(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var payload struct {
Query string `json:"query"`
Variables map[string]any `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
if !strings.Contains(payload.Query, "createSharedKey") {
t.Fatalf("query does not create shared key: %s", payload.Query)
}
if payload.Variables["entityId"] != testDeviceID {
t.Fatalf("unexpected entity id: %+v", payload.Variables)
}
input, ok := payload.Variables["input"].(map[string]any)
if !ok {
t.Fatalf("unexpected input: %+v", payload.Variables["input"])
}
if input["key"] != testClientKey || input["description"] != "provisioned from mg" {
t.Fatalf("unexpected input: %+v", input)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"createSharedKey": map[string]any{
"credentialId": testCredentialID,
"key": testClientKey,
},
},
})
}))
defer srv.Close()
client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
got, err := client.CreateSharedKey(context.Background(), testDeviceID, testClientKey, "provisioned from mg")
if err != nil {
t.Fatalf("create shared key failed: %v", err)
}
if got.CredentialID != testCredentialID || got.Key != testClientKey {
t.Fatalf("unexpected shared key response: %+v", got)
}
}
func TestRevealSharedKey(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var payload struct {
Query string `json:"query"`
Variables map[string]any `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
if !strings.Contains(payload.Query, "revealSharedKey") {
t.Fatalf("query does not reveal shared key: %s", payload.Query)
}
if payload.Variables["entityId"] != testDeviceID || payload.Variables["credentialId"] != testCredentialID {
t.Fatalf("unexpected variables: %+v", payload.Variables)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"revealSharedKey": map[string]any{
"credentialId": testCredentialID,
"key": testClientKey,
},
},
})
}))
defer srv.Close()
client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
got, err := client.RevealSharedKey(context.Background(), testDeviceID, testCredentialID)
if err != nil {
t.Fatalf("reveal shared key failed: %v", err)
}
if got.CredentialID != testCredentialID || got.Key != testClientKey {
t.Fatalf("unexpected shared key response: %+v", got)
}
}
func TestListCredentials(t *testing.T) {
createdAt := "2026-06-30T10:15:30Z"
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var payload struct {
Query string `json:"query"`
Variables map[string]any `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
if !strings.Contains(payload.Query, "credentials") {
t.Fatalf("query does not list credentials: %s", payload.Query)
}
if payload.Variables["entityId"] != testDeviceID {
t.Fatalf("unexpected variables: %+v", payload.Variables)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"credentials": map[string]any{
"total": 1,
"items": []map[string]any{{
"id": testCredentialID,
"entity_id": testDeviceID,
"kind": "shared_key",
"identifier": "",
"status": "active",
"created_at": createdAt,
}},
},
},
})
}))
defer srv.Close()
client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
got, err := client.ListCredentials(context.Background(), testDeviceID)
if err != nil {
t.Fatalf("list credentials failed: %v", err)
}
if got.Total != 1 || len(got.Items) != 1 {
t.Fatalf("unexpected credentials response: %+v", got)
}
item := got.Items[0]
if item.ID != testCredentialID || item.EntityID != testDeviceID || item.Kind != "shared_key" || item.Status != "active" {
t.Fatalf("unexpected credential item: %+v", item)
}
if item.CreatedAt.Format(time.RFC3339) != createdAt {
t.Fatalf("unexpected created_at: %s", item.CreatedAt.Format(time.RFC3339))
}
}
func TestLoadConfig(t *testing.T) { func TestLoadConfig(t *testing.T) {
t.Setenv("ATOM_URL", "http://atom:8080/") t.Setenv("ATOM_URL", "http://atom:8080/")
t.Setenv("ATOM_ADMIN_TOKEN", "token") t.Setenv("ATOM_ADMIN_TOKEN", "token")
+4
View File
@@ -6,5 +6,9 @@ package atom
const ( const (
testDeviceSecret = "device-secret" testDeviceSecret = "device-secret"
testDomainID = "domain-1" testDomainID = "domain-1"
testTenantID = "tenant-1"
testEntityID = "entity-1" testEntityID = "entity-1"
testDeviceID = "device-1"
testClientKey = "client-key"
testCredentialID = "cred-1"
) )
+21
View File
@@ -238,6 +238,27 @@ type APIKeyResponse struct {
ExpiresAt *time.Time `json:"expiresAt,omitempty"` ExpiresAt *time.Time `json:"expiresAt,omitempty"`
} }
type SharedKeyResponse struct {
CredentialID string `json:"credentialId"`
Key string `json:"key"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
}
type Credential struct {
ID string `json:"id"`
EntityID string `json:"entity_id,omitempty"`
Kind string `json:"kind"`
Identifier string `json:"identifier,omitempty"`
Status string `json:"status"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
}
type CredentialList struct {
Items []Credential `json:"items"`
Total uint64 `json:"total"`
}
type ResourceList struct { type ResourceList struct {
Items []Resource `json:"items"` Items []Resource `json:"items"`
Total uint64 `json:"total"` Total uint64 `json:"total"`