mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 07:14:46 +00:00
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
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:
@@ -193,6 +193,8 @@ jobs:
|
||||
base-url: ${{ env.USERS_URL }}
|
||||
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'
|
||||
coverage-artifact-name: schema-coverage-users
|
||||
coverage-pr-comment: false
|
||||
|
||||
- name: Run Groups API tests
|
||||
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 }}
|
||||
checks: all
|
||||
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
|
||||
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 }}
|
||||
checks: all
|
||||
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
|
||||
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 }}
|
||||
checks: all
|
||||
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
|
||||
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 }}
|
||||
checks: all
|
||||
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
|
||||
if: steps.changes.outputs.journal == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
@@ -238,6 +248,8 @@ jobs:
|
||||
base-url: ${{ env.JOURNAL_URL }}
|
||||
checks: all
|
||||
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
|
||||
if: (steps.changes.outputs.bootstrap == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('bootstrap/api/**') != ''
|
||||
@@ -247,6 +259,8 @@ jobs:
|
||||
base-url: ${{ env.BOOTSTRAP_URL }}
|
||||
checks: all
|
||||
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
|
||||
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 }}
|
||||
checks: all
|
||||
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
|
||||
if: steps.changes.outputs.readers == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
@@ -265,6 +281,8 @@ jobs:
|
||||
base-url: ${{ env.READERS_URL }}
|
||||
checks: all
|
||||
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
|
||||
if: steps.changes.outputs.re == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
@@ -274,6 +292,8 @@ jobs:
|
||||
base-url: ${{ env.RE_URL }}
|
||||
checks: all
|
||||
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
|
||||
if: steps.changes.outputs.alarms == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
@@ -283,6 +303,8 @@ jobs:
|
||||
base-url: ${{ env.ALARMS_URL }}
|
||||
checks: all
|
||||
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
|
||||
if: steps.changes.outputs.reports == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
@@ -292,6 +314,8 @@ jobs:
|
||||
base-url: ${{ env.REPORTS_URL }}
|
||||
checks: all
|
||||
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
|
||||
if: always()
|
||||
|
||||
+1
-75
@@ -9,7 +9,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -54,40 +53,6 @@ type config struct {
|
||||
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() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
@@ -200,45 +165,6 @@ func main() {
|
||||
}
|
||||
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"}
|
||||
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))
|
||||
@@ -250,7 +176,7 @@ func main() {
|
||||
cancel,
|
||||
"fluxmq-publish",
|
||||
httpServerConfig,
|
||||
fluxmqhttp.MakePublishHandler(authn, atomAuthz, publisher),
|
||||
fluxmqhttp.MakePublishHandler(authn, atomAuthz, messagePublisher),
|
||||
logger,
|
||||
)
|
||||
|
||||
|
||||
+68
-10
@@ -56,7 +56,7 @@ func (c *Client) CreateTenant(ctx context.Context, tenant Tenant) (Tenant, error
|
||||
CreateTenant Tenant `json:"createTenant"`
|
||||
}
|
||||
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)
|
||||
return out.CreateTenant, err
|
||||
}
|
||||
@@ -66,7 +66,7 @@ func (c *Client) GetTenant(ctx context.Context, id string) (Tenant, error) {
|
||||
Tenant Tenant `json:"tenant"`
|
||||
}
|
||||
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)
|
||||
return out.Tenant, err
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func (c *Client) UpdateTenant(ctx context.Context, id string, tenant Tenant) (Te
|
||||
UpdateTenant Tenant `json:"updateTenant"`
|
||||
}
|
||||
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)
|
||||
return out.UpdateTenant, err
|
||||
}
|
||||
@@ -92,7 +92,7 @@ func (c *Client) ChangeTenantStatus(ctx context.Context, id, action string) (Ten
|
||||
}
|
||||
var out map[string]Tenant
|
||||
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)
|
||||
if err != nil {
|
||||
return Tenant{}, err
|
||||
@@ -234,10 +234,10 @@ func (c *Client) ListTenants(ctx context.Context, q Query) (TenantList, error) {
|
||||
var out struct {
|
||||
Tenants TenantList `json:"tenants"`
|
||||
}
|
||||
err := c.graphQL(ctx, `query Tenants($q: String, $name: String, $route: String, $status: TenantStatus, $limit: Int, $offset: Int) {
|
||||
tenants(q: $q, name: $name, route: $route, status: $status, limit: $limit, offset: $offset) {
|
||||
err := c.graphQL(ctx, `query Tenants($q: String, $name: String, $alias: String, $status: TenantStatus, $limit: Int, $offset: Int) {
|
||||
tenants(q: $q, name: $name, alias: $alias, status: $status, limit: $limit, offset: $offset) {
|
||||
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)
|
||||
return out.Tenants, err
|
||||
@@ -506,6 +506,64 @@ func (c *Client) CreateAPIKey(ctx context.Context, entityID, description string)
|
||||
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 {
|
||||
return c.graphQL(ctx, `mutation RevokeCredential($entityId: ID!, $credentialId: ID!) {
|
||||
revokeCredential(entityId: $entityId, credentialId: $credentialId)
|
||||
@@ -632,7 +690,7 @@ func graphQLErr(errors []graphQLErrorItem) error {
|
||||
func tenantCreateInput(tenant Tenant) map[string]any {
|
||||
input := map[string]any{"name": tenant.Name}
|
||||
setIfNotEmpty(input, "id", tenant.ID)
|
||||
setIfNotEmpty(input, "route", tenant.Route)
|
||||
setIfNotEmpty(input, "alias", tenant.Route)
|
||||
if tenant.Tags != nil {
|
||||
input["tags"] = tenant.Tags
|
||||
}
|
||||
@@ -645,7 +703,7 @@ func tenantCreateInput(tenant Tenant) map[string]any {
|
||||
func tenantUpdateInput(tenant Tenant) map[string]any {
|
||||
input := map[string]any{}
|
||||
setIfNotEmpty(input, "name", tenant.Name)
|
||||
setIfNotEmpty(input, "route", tenant.Route)
|
||||
setIfNotEmpty(input, "alias", tenant.Route)
|
||||
if tenant.Tags != nil {
|
||||
input["tags"] = tenant.Tags
|
||||
}
|
||||
@@ -798,7 +856,7 @@ func queryVariables(q Query) map[string]any {
|
||||
vars := map[string]any{}
|
||||
setIfNotEmpty(vars, "q", q.Q)
|
||||
setIfNotEmpty(vars, "name", q.Name)
|
||||
setIfNotEmpty(vars, "route", q.Route)
|
||||
setIfNotEmpty(vars, "alias", q.Route)
|
||||
setIfNotEmpty(vars, "kind", q.Kind)
|
||||
setIfNotEmpty(vars, "tenantId", q.TenantID)
|
||||
setIfNotEmpty(vars, "status", q.Status)
|
||||
|
||||
@@ -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) {
|
||||
t.Setenv("ATOM_URL", "http://atom:8080/")
|
||||
t.Setenv("ATOM_ADMIN_TOKEN", "token")
|
||||
|
||||
@@ -6,5 +6,9 @@ package atom
|
||||
const (
|
||||
testDeviceSecret = "device-secret"
|
||||
testDomainID = "domain-1"
|
||||
testTenantID = "tenant-1"
|
||||
testEntityID = "entity-1"
|
||||
testDeviceID = "device-1"
|
||||
testClientKey = "client-key"
|
||||
testCredentialID = "cred-1"
|
||||
)
|
||||
|
||||
@@ -238,6 +238,27 @@ type APIKeyResponse struct {
|
||||
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 {
|
||||
Items []Resource `json:"items"`
|
||||
Total uint64 `json:"total"`
|
||||
|
||||
Reference in New Issue
Block a user