diff --git a/tests/integration/resolvers/sharedCampaignCrud.integration.test.ts b/tests/integration/resolvers/sharedCampaignCrud.integration.test.ts index 38aa57fa..ed682435 100644 --- a/tests/integration/resolvers/sharedCampaignCrud.integration.test.ts +++ b/tests/integration/resolvers/sharedCampaignCrud.integration.test.ts @@ -437,6 +437,33 @@ describe('Shared Campaign CRUD Operations', () => { expect(result.data.getSharedCampaign).toBeNull(); }); + + it('should reject retrieval by unauthenticated user', async () => { + const unauthClient = createUnauthenticatedClient(); + + await expect( + unauthClient.query({ + query: GET_CAMPAIGN_SHARED_CAMPAIGN, + variables: { sharedCampaignCode }, + }) + ).rejects.toThrow(); + }); + + it('should allow any authenticated user to retrieve by code (code-as-capability redemption)', async () => { + // The shared campaign code is the bearer capability (same model as profile + // invites): any authenticated user holding the code may view/redeem it. + const otherResult = await createAuthenticatedClient('contributor'); + + const result = await otherResult.client.query({ + query: GET_CAMPAIGN_SHARED_CAMPAIGN, + variables: { sharedCampaignCode }, + }); + + expect(result.data.getSharedCampaign).toBeDefined(); + expect(result.data.getSharedCampaign.sharedCampaignCode).toBe(sharedCampaignCode); + + // NOTE: Do NOT delete test accounts - they are shared across test runs + }); }); describe('ListMySharedCampaigns', () => { diff --git a/tofu/application/appsync/mapping-templates/get_shared_campaign_request.vtl b/tofu/application/appsync/mapping-templates/get_shared_campaign_request.vtl index 745a931f..5dd6624e 100644 --- a/tofu/application/appsync/mapping-templates/get_shared_campaign_request.vtl +++ b/tofu/application/appsync/mapping-templates/get_shared_campaign_request.vtl @@ -1,3 +1,8 @@ +## Only authenticated users may view/redeem a shared campaign. +## The shared campaign code acts as the bearer capability (same model as profile invites). +#if(!$ctx.identity) + $util.unauthorized() +#end { "version": "2017-02-28", "operation": "GetItem", diff --git a/tofu/application/schema/schema.graphql b/tofu/application/schema/schema.graphql index c9d9e35c..3a216f7d 100644 --- a/tofu/application/schema/schema.graphql +++ b/tofu/application/schema/schema.graphql @@ -430,7 +430,7 @@ type Query { listInvitesByProfile(profileId: ID!): [ProfileInvite!]! # Shared campaign queries - getSharedCampaign(sharedCampaignCode: String!): SharedCampaign + getSharedCampaign(sharedCampaignCode: String!): SharedCampaign @aws_cognito_user_pools listMySharedCampaigns: [SharedCampaign!]! findSharedCampaigns(unitType: String!, unitNumber: Int!, city: String!, state: String!, campaignName: String!, campaignYear: Int!): [SharedCampaign!]!