diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 2015939..2a977d2 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -39,6 +39,7 @@ jobs: TEST_USER_ID: ${{ secrets.TEST_USER_ID }} TEST_USER_REFRESH_TOKEN: ${{ secrets.TEST_USER_REFRESH_TOKEN }} AUTH_REDIRECT_URI: 'https://test/callback/' + CREATE_PLAYLIST_LAMBDA_ARN: 'arn:test' - name: Upload build artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/cypress-tests.yml b/.github/workflows/cypress-tests.yml index c56155c..f001c72 100644 --- a/.github/workflows/cypress-tests.yml +++ b/.github/workflows/cypress-tests.yml @@ -36,3 +36,4 @@ jobs: SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }} TEST_USER_REFRESH_TOKEN: ${{ secrets.TEST_USER_REFRESH_TOKEN }} AUTH_REDIRECT_URI: 'https://${{ inputs.domain }}/callback.html' + CREATE_PLAYLIST_LAMBDA_ARN: 'arn:test' diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 169bd6d..f6212ba 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -48,6 +48,7 @@ jobs: TF_VAR_spotify_client_id: ${{ secrets.SPOTIFY_CLIENT_ID }} TF_VAR_spotify_client_secret: ${{ secrets.SPOTIFY_CLIENT_SECRET }} TF_VAR_auth_redirect_uri: 'https://d1e7htx1c4j3w0.cloudfront.net/callback.html' + TF_VAR_create_playlist_lambda_arn: 'arn:aws:lambda:us-east-1:765212935426:function:all-tracks-create-playlist-processor' - name: Terraform Apply run: terraform apply -input=false -auto-approve @@ -57,6 +58,7 @@ jobs: TF_VAR_spotify_client_id: ${{ secrets.SPOTIFY_CLIENT_ID }} TF_VAR_spotify_client_secret: ${{ secrets.SPOTIFY_CLIENT_SECRET }} TF_VAR_auth_redirect_uri: 'https://d1e7htx1c4j3w0.cloudfront.net/callback.html' + TF_VAR_create_playlist_lambda_arn: 'arn:aws:lambda:us-east-1:765212935426:function:all-tracks-create-playlist-processor' - name: Get Terraform outputs id: tf-outputs diff --git a/.github/workflows/large-tests.yml b/.github/workflows/large-tests.yml index ac34c13..0fce9cc 100644 --- a/.github/workflows/large-tests.yml +++ b/.github/workflows/large-tests.yml @@ -41,3 +41,4 @@ jobs: TEST_USER_ID: ${{ secrets.TEST_USER_ID }} TEST_USER_REFRESH_TOKEN: ${{ secrets.TEST_USER_REFRESH_TOKEN }} AUTH_REDIRECT_URI: 'https://test/callback/' + CREATE_PLAYLIST_LAMBDA_ARN: 'arn:aws:lambda:us-east-1:765212935426:function:all-tracks-create-playlist-processor' diff --git a/cypress/e2e/index.cy.js b/cypress/e2e/index.cy.js index a5b31f8..16cb40d 100644 --- a/cypress/e2e/index.cy.js +++ b/cypress/e2e/index.cy.js @@ -54,6 +54,9 @@ describe('index', () => { process.env.SPOTIFY_CLIENT_ID = Cypress.env('SPOTIFY_CLIENT_ID'); process.env.SPOTIFY_CLIENT_SECRET = Cypress.env('SPOTIFY_CLIENT_SECRET'); process.env.AUTH_REDIRECT_URI = Cypress.env('AUTH_REDIRECT_URI'); + process.env.CREATE_PLAYLIST_LAMBDA_ARN = Cypress.env( + 'CREATE_PLAYLIST_LAMBDA_ARN' + ); const accessToken = await refreshToken( Cypress.env('TEST_USER_REFRESH_TOKEN') ); @@ -70,6 +73,9 @@ describe('index', () => { process.env.SPOTIFY_CLIENT_ID = Cypress.env('SPOTIFY_CLIENT_ID'); process.env.SPOTIFY_CLIENT_SECRET = Cypress.env('SPOTIFY_CLIENT_SECRET'); process.env.AUTH_REDIRECT_URI = Cypress.env('AUTH_REDIRECT_URI'); + process.env.CREATE_PLAYLIST_LAMBDA_ARN = Cypress.env( + 'CREATE_PLAYLIST_LAMBDA_ARN' + ); const accessToken = await refreshToken( Cypress.env('TEST_USER_REFRESH_TOKEN') ); @@ -84,11 +90,14 @@ describe('index', () => { ).should('not.be.visible'); }); - it('creates a playlist when the create playlist button is clicked', () => { + it("informs user playlist will be available soon when 'Create Playlist' is clicked", () => { cy.wrap(null).then(async () => { process.env.SPOTIFY_CLIENT_ID = Cypress.env('SPOTIFY_CLIENT_ID'); process.env.SPOTIFY_CLIENT_SECRET = Cypress.env('SPOTIFY_CLIENT_SECRET'); process.env.AUTH_REDIRECT_URI = Cypress.env('AUTH_REDIRECT_URI'); + process.env.CREATE_PLAYLIST_LAMBDA_ARN = Cypress.env( + 'CREATE_PLAYLIST_LAMBDA_ARN' + ); const accessToken = await refreshToken( Cypress.env('TEST_USER_REFRESH_TOKEN') ); @@ -122,25 +131,13 @@ describe('index', () => { // Wait for the request to complete cy.wait('@createPlaylistRequest', { timeout: 20000 }).then( (interception) => { - // Verify the response contains the expected data - expect(interception.response.statusCode).to.equal(201); - expect(interception.response.body).to.have.property( - 'name', - 'The Beatles - All tracks' - ); - expect(interception.response.body).to.have.property( - 'description', - 'Playlist created by all-tracks: https://d1e7htx1c4j3w0.cloudfront.net' - ); - expect(interception.response.body).to.have.property('url'); + // Verify the response is 200 + expect(interception.response.statusCode).to.equal(200); - // Verify the result is displayed on the page - cy.contains('Playlist created successfully!').should('be.visible'); - cy.contains(interception.response.body.name).should('be.visible'); - cy.contains(interception.response.body.description).should( - 'be.visible' - ); - cy.contains(interception.response.body.url).should('be.visible'); + // Verify the result message is displayed on the page + cy.contains( + 'Your playlist will be available in your Spotify account in a few minutes!' + ).should('be.visible'); } ); }); diff --git a/deploy/iam.tf b/deploy/iam.tf index 62bbcff..9726765 100644 --- a/deploy/iam.tf +++ b/deploy/iam.tf @@ -26,6 +26,23 @@ resource "aws_iam_role_policy_attachment" "lambda_basic" { role = aws_iam_role.lambda_role.name } +# IAM policy to allow API Lambda to invoke Create Playlist Lambda +resource "aws_iam_role_policy" "lambda_invoke_create_playlist" { + name = "${local.app_name}-invoke-create-playlist-policy" + role = aws_iam_role.lambda_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "lambda:InvokeFunction" + Effect = "Allow" + Resource = var.create_playlist_lambda_arn + } + ] + }) +} + # # Create Playlist Processor Lambda IAM Resources diff --git a/deploy/lambda.tf b/deploy/lambda.tf index e758590..0eff00e 100644 --- a/deploy/lambda.tf +++ b/deploy/lambda.tf @@ -11,9 +11,10 @@ resource "aws_lambda_function" "api_lambda" { environment { variables = { - SPOTIFY_CLIENT_ID = var.spotify_client_id - SPOTIFY_CLIENT_SECRET = var.spotify_client_secret - AUTH_REDIRECT_URI = var.auth_redirect_uri + SPOTIFY_CLIENT_ID = var.spotify_client_id + SPOTIFY_CLIENT_SECRET = var.spotify_client_secret + AUTH_REDIRECT_URI = var.auth_redirect_uri + CREATE_PLAYLIST_LAMBDA_ARN = var.create_playlist_lambda_arn } } @@ -43,15 +44,16 @@ resource "aws_lambda_function" "create_playlist_processor_lambda" { role = aws_iam_role.create_playlist_processor_role.arn handler = "index.handleCreatePlaylistRequest" runtime = "nodejs18.x" - timeout = 30 + timeout = 600 source_code_hash = data.archive_file.create_playlist_processor_lambda_zip.output_base64sha256 memory_size = 1024 environment { variables = { - SPOTIFY_CLIENT_ID = var.spotify_client_id - SPOTIFY_CLIENT_SECRET = var.spotify_client_secret - AUTH_REDIRECT_URI = var.auth_redirect_uri + SPOTIFY_CLIENT_ID = var.spotify_client_id + SPOTIFY_CLIENT_SECRET = var.spotify_client_secret + AUTH_REDIRECT_URI = var.auth_redirect_uri + CREATE_PLAYLIST_LAMBDA_ARN = var.create_playlist_lambda_arn } } diff --git a/deploy/variables.tf b/deploy/variables.tf index c01b958..b269939 100644 --- a/deploy/variables.tf +++ b/deploy/variables.tf @@ -10,3 +10,7 @@ variable "spotify_client_secret" { variable "auth_redirect_uri" { type = string } + +variable "create_playlist_lambda_arn" { + type = string +} diff --git a/jest.config.js b/jest.config.js index c4bcc0f..3ca47dc 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,5 +3,5 @@ module.exports = { testEnvironment: 'node', testMatch: ['**/?(*.)+(spec|test).ts'], setupFiles: ['dotenv/config'], - testTimeout: 20000 + testTimeout: 30000 }; diff --git a/src/apiRequestHandlers/handleCreatePlaylistRequest.ts b/src/apiRequestHandlers/handleCreatePlaylistRequest.ts index 0a85eec..8904f1d 100644 --- a/src/apiRequestHandlers/handleCreatePlaylistRequest.ts +++ b/src/apiRequestHandlers/handleCreatePlaylistRequest.ts @@ -1,7 +1,7 @@ import { generateResult, ApiEvent } from '../handleApiRequest'; import { APIGatewayProxyResult } from 'aws-lambda'; import { getAccessTokenFromEvent } from './getAccessTokenFromEvent'; -import { createPlaylist } from '../spotify/createPlaylist'; +import { invokeCreatePlaylistLambda } from '../invokeCreatePlaylistLambda'; export const handleCreatePlaylistRequest = async ( event: ApiEvent @@ -22,10 +22,10 @@ export const handleCreatePlaylistRequest = async ( }); } - const result = await createPlaylist({ accessToken, artistId }); + await invokeCreatePlaylistLambda({ accessToken, artistId }); return generateResult({ - statusCode: 201, - bodyObject: result + statusCode: 200, + bodyObject: { message: 'Playlist creation accepted' } }); }; diff --git a/src/config/getEnvVariables.small.test.ts b/src/config/getEnvVariables.small.test.ts index b092573..d010415 100644 --- a/src/config/getEnvVariables.small.test.ts +++ b/src/config/getEnvVariables.small.test.ts @@ -4,36 +4,42 @@ describe('getEnvVariables', () => { const expectedClientId = 'client_id'; const expectedClientSecret = 'secret'; const expectedRedirectUri = 'redirect_uri'; + const expectedLambdaArn = 'lambda_arn'; beforeEach(() => { process.env.SPOTIFY_CLIENT_ID = expectedClientId; process.env.SPOTIFY_CLIENT_SECRET = expectedClientSecret; process.env.AUTH_REDIRECT_URI = expectedRedirectUri; + process.env.CREATE_PLAYLIST_LAMBDA_ARN = expectedLambdaArn; }); it('gets env variables', async () => { - const { spotifyClientId, spotifyClientSecret, authRedirectUri } = + const { spotifyClientId, spotifyClientSecret, authRedirectUri, createPlaylistLambdaArn } = getEnvVariables(); expect(spotifyClientId).toBe(expectedClientId); expect(spotifyClientSecret).toBe(expectedClientSecret); expect(authRedirectUri).toBe(expectedRedirectUri); + expect(createPlaylistLambdaArn).toBe(expectedLambdaArn); }); it('gets different env variables', async () => { const differentClientId = 'different_client_id'; const differentClientSecret = 'different_secret'; const differentRedirectUri = 'different_redirect_uri'; + const differentLambdaArn = 'different_lambda_arn'; process.env.SPOTIFY_CLIENT_ID = differentClientId; process.env.SPOTIFY_CLIENT_SECRET = differentClientSecret; process.env.AUTH_REDIRECT_URI = differentRedirectUri; + process.env.CREATE_PLAYLIST_LAMBDA_ARN = differentLambdaArn; - const { spotifyClientId, spotifyClientSecret, authRedirectUri } = + const { spotifyClientId, spotifyClientSecret, authRedirectUri, createPlaylistLambdaArn } = getEnvVariables(); expect(spotifyClientId).toBe(differentClientId); expect(spotifyClientSecret).toBe(differentClientSecret); expect(authRedirectUri).toBe(differentRedirectUri); + expect(createPlaylistLambdaArn).toBe(differentLambdaArn); }); it('throws error if SPOTIFY_CLIENT_ID not set', async () => { @@ -59,4 +65,12 @@ describe('getEnvVariables', () => { 'AUTH_REDIRECT_URI environment variable is not set' ); }); + + it('throws error if CREATE_PLAYLIST_LAMBDA_ARN not set', async () => { + delete process.env.CREATE_PLAYLIST_LAMBDA_ARN; + + expect(getEnvVariables).toThrow( + 'CREATE_PLAYLIST_LAMBDA_ARN environment variable is not set' + ); + }); }); diff --git a/src/config/getEnvVariables.ts b/src/config/getEnvVariables.ts index 4532bb1..b8f1eb2 100644 --- a/src/config/getEnvVariables.ts +++ b/src/config/getEnvVariables.ts @@ -11,10 +11,15 @@ export const getEnvVariables = (): EnvVariables => { throw new Error('AUTH_REDIRECT_URI environment variable is not set'); } + if (process.env.CREATE_PLAYLIST_LAMBDA_ARN === undefined) { + throw new Error('CREATE_PLAYLIST_LAMBDA_ARN environment variable is not set'); + } + return { spotifyClientId: process.env.SPOTIFY_CLIENT_ID, spotifyClientSecret: process.env.SPOTIFY_CLIENT_SECRET, - authRedirectUri: process.env.AUTH_REDIRECT_URI + authRedirectUri: process.env.AUTH_REDIRECT_URI, + createPlaylistLambdaArn: process.env.CREATE_PLAYLIST_LAMBDA_ARN }; }; @@ -22,4 +27,5 @@ type EnvVariables = { spotifyClientId: string; spotifyClientSecret: string; authRedirectUri: string; + createPlaylistLambdaArn: string; }; diff --git a/src/handleApiRequest.small.test.ts b/src/handleApiRequest.small.test.ts index 1492473..018ad69 100644 --- a/src/handleApiRequest.small.test.ts +++ b/src/handleApiRequest.small.test.ts @@ -5,14 +5,14 @@ import { getMe } from './spotify/getMe'; import { getAccessToken } from './spotify/auth/getAccessToken'; import { when } from 'jest-when'; import { getArtistsTracks } from './spotify/getArtistsTracks'; -import { createPlaylist } from './spotify/createPlaylist'; +import { invokeCreatePlaylistLambda } from './invokeCreatePlaylistLambda'; jest.mock('./spotify/getArtistName'); jest.mock('./spotify/getArtistsAlbums'); jest.mock('./spotify/getMe'); jest.mock('./spotify/auth/getAccessToken'); jest.mock('./spotify/getArtistsTracks'); -jest.mock('./spotify/createPlaylist'); +jest.mock('./invokeCreatePlaylistLambda'); const accessToken = 'accessToken'; const authorizationHeader = `Bearer ${accessToken}`; @@ -456,25 +456,8 @@ describe('apiHandler', () => { }); }); - it('creates a playlist and returns the url to it', async () => { - const name = 'playlistName'; - const description = 'playlistDescription'; - const url = 'playlistUrl'; - const id = 'playlistId'; + it('invokes create playlist lambda and returns acceptance response', async () => { const artistId = 'artistId'; - - when(createPlaylist) - .calledWith({ - accessToken, - artistId - }) - .mockResolvedValue({ - id, - name, - description, - url - }); - const mockEvent: ApiEvent = { queryStringParameters: { artistId @@ -484,19 +467,21 @@ describe('apiHandler', () => { Authorization: authorizationHeader } }; + const response = await handleApiRequest(mockEvent); + expect(invokeCreatePlaylistLambda).toHaveBeenCalledWith({ + accessToken, + artistId + }); expect(response).toStrictEqual({ - statusCode: 201, + statusCode: 200, headers: { 'Content-Type': 'application/json' }, multiValueHeaders: {}, body: JSON.stringify({ - id, - name, - description, - url + message: 'Playlist creation accepted' }) }); }); diff --git a/src/invokeCreatePlaylistLambda.ts b/src/invokeCreatePlaylistLambda.ts new file mode 100644 index 0000000..cbb1cdb --- /dev/null +++ b/src/invokeCreatePlaylistLambda.ts @@ -0,0 +1,22 @@ +import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda'; +import { getEnvVariables } from './config/getEnvVariables'; + +const lambdaClient = new LambdaClient({}); + +export const invokeCreatePlaylistLambda = async ({ + accessToken, + artistId +}: { + accessToken: string; + artistId: string; +}) => { + const { createPlaylistLambdaArn } = getEnvVariables(); + + const invokeCommand = new InvokeCommand({ + FunctionName: createPlaylistLambdaArn, + InvocationType: 'Event', + Payload: JSON.stringify({ accessToken, artistId }) + }); + + await lambdaClient.send(invokeCommand); +}; diff --git a/src/spotify/auth/getAccessToken.small.test.ts b/src/spotify/auth/getAccessToken.small.test.ts index 57e8e36..015990d 100644 --- a/src/spotify/auth/getAccessToken.small.test.ts +++ b/src/spotify/auth/getAccessToken.small.test.ts @@ -11,6 +11,7 @@ describe('getAccessToken', () => { process.env.SPOTIFY_CLIENT_ID = 'test_client_id'; process.env.SPOTIFY_CLIENT_SECRET = 'test_client_secret'; process.env.AUTH_REDIRECT_URI = 'test_auth_redirect_uri'; + process.env.CREATE_PLAYLIST_LAMBDA_ARN = 'test_lambda_arn'; const expectedBasicToken = generateBasicToken(); const expectedAuthCode = 'test_auth_code'; diff --git a/src/web/script.js b/src/web/script.js index b6934cc..4183aa9 100644 --- a/src/web/script.js +++ b/src/web/script.js @@ -64,14 +64,12 @@ function createPlaylist() { method: 'POST', credentials: 'include' } - ) - .then((response) => response.json()) - .then((data) => { + ).then((response) => { + if (response.status === 200) { resultElement.innerHTML = ` -

Playlist created successfully!

-

Name: ${data.name}

-

Description: ${data.description}

-

URL: ${data.url}

- `; - }); +

Your playlist will be available in your Spotify account in a few minutes!

+ `; + return null; + } + }); } diff --git a/tests/large/api.large.test.ts b/tests/large/api.large.test.ts index bf8ecdb..b3c7b22 100644 --- a/tests/large/api.large.test.ts +++ b/tests/large/api.large.test.ts @@ -186,17 +186,11 @@ describe('api', () => { } }); - expect(response.status).toBe(201); + expect(response.status).toBe(200); const responseBodyText = await response.text(); const responseObject = JSON.parse(responseBodyText); - expect(responseObject.name).toBe('The Beatles - All tracks'); - expect(responseObject.description).toBe( - 'Playlist created by all-tracks: https://d1e7htx1c4j3w0.cloudfront.net' - ); - expect(responseObject.url).toContain( - 'https://open.spotify.com/playlist/' - ); + expect(responseObject.message).toBe('Playlist creation accepted'); }); it("creates a playlist with all The Beatles' tracks when using cookies for auth", async () => { @@ -212,17 +206,11 @@ describe('api', () => { } }); - expect(response.status).toBe(201); + expect(response.status).toBe(200); const responseBodyText = await response.text(); const responseObject = JSON.parse(responseBodyText); - expect(responseObject.name).toBe('The Beatles - All tracks'); - expect(responseObject.description).toBe( - 'Playlist created by all-tracks: https://d1e7htx1c4j3w0.cloudfront.net' - ); - expect(responseObject.url).toContain( - 'https://open.spotify.com/playlist/' - ); + expect(responseObject.message).toBe('Playlist creation accepted'); }); }); }); diff --git a/tests/large/invokeCreatePlaylistLambda.large.test.ts b/tests/large/invokeCreatePlaylistLambda.large.test.ts new file mode 100644 index 0000000..7cf7165 --- /dev/null +++ b/tests/large/invokeCreatePlaylistLambda.large.test.ts @@ -0,0 +1,21 @@ +import { refreshToken } from '../../src/spotify/auth/refreshToken'; +import { invokeCreatePlaylistLambda } from '../../src/invokeCreatePlaylistLambda'; + +const theBeatlesArtistId = '3WrFJ7ztbogyGnTHbHJFl2'; +let accessToken: string; + +describe('invokeCreatePlaylistLambda', () => { + beforeAll(async () => { + process.env.CREATE_PLAYLIST_LAMBDA_ARN = 'all-tracks-create-playlist-processor'; + accessToken = await refreshToken(process.env.TEST_USER_REFRESH_TOKEN!); + }); + + it('invokes the lambda asynchronously without throwing error', async () => { + await expect( + invokeCreatePlaylistLambda({ + accessToken, + artistId: theBeatlesArtistId + }) + ).resolves.not.toThrow(); + }); +});