├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── amplify.yml ├── media ├── api-step.png ├── arch-new.png ├── arch_22_07_24.png ├── email.png ├── sns_subscription.png └── step_22_07_24.png └── src ├── backend ├── .gitignore ├── AWSStepFunctionsPlagiarismDemo.sln ├── AdminAction.Tests │ ├── AdminAction.Tests.csproj │ └── FunctionTests.cs ├── AdminAction │ ├── AdminAction.csproj │ └── Function.cs ├── Plagiarism │ ├── ExamNotFoundException.cs │ ├── Incident.cs │ ├── IncidentNotFoundException.cs │ ├── Plagiarism.csproj │ └── StudentExceededAllowableExamRetries.cs ├── PlagiarismRepository.Tests │ ├── PlagiarismRepository.Tests.csproj │ ├── RepositoryTests.cs │ └── docker-compose.yml ├── PlagiarismRepository │ ├── IIncidentRepository.cs │ ├── IncidentRepository.cs │ └── PlagiarismRepository.csproj ├── README.md ├── ResolveIncident.Tests │ ├── FunctionTests.cs │ └── ResolveIncident.Tests.csproj ├── ResolveIncident │ ├── Function.cs │ └── ResolveIncident.csproj ├── ScheduleExam.Tests │ ├── FunctionTests.cs │ └── ScheduleExam.Tests.csproj ├── ScheduleExam │ ├── Function.cs │ └── ScheduleExam.csproj ├── SubmitExam.Tests │ ├── FunctionTests.cs │ ├── SubmitExam.Tests.csproj │ └── event.json ├── SubmitExam │ ├── Function.cs │ └── SubmitExam.csproj ├── api.yaml ├── omnisharp.json ├── state-machine.asl.yaml └── template.yaml └── frontend ├── .env ├── .eslintrc.json ├── .gitignore ├── README.md ├── amplify.yml ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── public ├── images │ ├── AWS_logo_RGB_REV.png │ ├── aws-smile.png │ ├── bg.png │ ├── favicon.ico │ ├── header-icon_step-functions.png │ └── stepfunction.png ├── index.html └── next.svg ├── src ├── api │ └── api.ts └── app │ ├── admin │ ├── custom.scss │ ├── layout.tsx │ └── page.tsx │ ├── globals.scss │ ├── layout.tsx │ ├── page.tsx │ └── testcentre │ ├── components │ ├── Exam.tsx │ ├── ExamIntegration.tsx │ ├── Question.tsx │ └── questionData.json │ ├── custom.scss │ └── page.tsx ├── testcentre-screenshot.png └── tsconfig.json /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" # See documentation for possible values 9 | directory: "/" # Location of the packages 10 | schedule: 11 | interval: "monthly" 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ develop, main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ develop ] 20 | schedule: 21 | - cron: '30 1 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'csharp', 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v2 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v2 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | **/.DS_Store 4 | bin/ 5 | pkg/** -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-samples/aws-step-functions-plagiarism-demo-dotnetcore/issues), or [recently closed](https://github.com/aws-samples/aws-step-functions-plagiarism-demo-dotnetcore/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-samples/aws-step-functions-plagiarism-demo-dotnetcore/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-samples/aws-step-functions-plagiarism-demo-dotnetcore/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Step Functions Plagiarism Demo for .NET 2 | 3 | This sample seeks to demonstrate how you can use implement a simple workflow using [AWS Step Functions](https://aws.amazon.com/step-functions/) and other AWS services, that involves human interaction. 4 | 5 | It demonstrates how you can combine Step Functions, using the [service integration callback pattern](https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-wait-token) to interface with a website that provides additional information throught the execution of the workflow, with [AWS Lambda](https://aws.amazon.com/lambda/), [Amazon DynamoDB](https://aws.amazon.com/dynamodb/), and [Powertools for AWS](https://docs.powertools.aws.dev/lambda/dotnet/), using the latest version of [Microsoft .NET](https://dotnet.microsoft.com/). 6 | 7 | You will also see how we use the [AWS Serverless Application Model (SAM)](https://github.com/awslabs/serverless-application-model) to define and model your Serverless application, and use [AWS SAM CLI](https://github.com/awslabs/aws-sam-cli) to build and deploy it. 8 | 9 | ## The Scenario 10 | 11 | The scenario is based on a real-world scenario where students are caught plagiarising on exams and/or assignments are required to take a test to assess their knowledge of the universities referencing standards. This application manages the incidents and the students' attempts to pass the exam. 12 | 13 | The process starts by: 14 | 15 | 1. University staff registering the plagiarism incident 16 | 1. The application schedules an exam. Students have one week to complete the test 17 | 1. Student are sent an email notification to inform them of the requirement to sit the exam 18 | 1. The process waits for the student to complete the exam, then 19 | 1. Determines whether or not the student has sat the exam, passed or failed. 20 | 1. If the student has failed the exam they are allowed to resit the exam. Students get 3 attempts to pass the exam before the incident is either resolved, or administrative action is taken. 21 | 22 | Visually, the process looks like this: 23 | 24 | ![Developing With Step Functions](media/step_22_07_24.png "Developing With Step Functions") 25 | 26 | ### The Architecture 27 | 28 | The solution has three main comonents: 29 | 30 | 1. The Serverless backend application 31 | 1. The "admin" website (http://localhost:3000/admin) which is used by university staff to register plagiarism incidents. 32 | 1. The "testing centre" website (http://localhost:3000/) which is used by students to sit the exam. 33 | 34 | ![Developing With Step Functions Architecture](media/arch_22_07_24.png "Developing With Step Functions Architecture") 35 | 36 | The incident captured at via the admin website initiates the AWS Step Function execution through an AWS Service integration on the `/incident` resource for the `POST` method. 37 | 38 | ![Integration Request](media/api-step.png "Integration Request") 39 | 40 | Once the the exam is scheduled, we use an Amazon SNS Integration Task with a `.waitForTaskToken` (see [AWS docs](https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-wait-token)). The Task Token is passed to the function (using built in reference of `$$.Task.Token`) which in turn generates the email notifying the student of the exam requirements. 41 | 42 | The result of the exam pass. Here is a sample from the state machine: 43 | 44 | ``` yaml 45 | Notify student: 46 | Type: Task 47 | Resource: "arn:aws:states:::sns:publish.waitForTaskToken" 48 | Parameters: 49 | TopicArn: '${NotificationTopic}' 50 | Message.$: "States.Format('http://localhost:3000/?IncidentId={}&ExamId={}&TaskToken={}', $.Payload.IncidentId, $.Payload.Exams[0].ExamId, $$.Task.Token)" 51 | Next: Has student passed exam? 52 | ``` 53 | 54 | Once the student receives the email, the Task Token is passed to the Testing Centre. The student answers the questions and submits the results to the `/exam` resource on the API. The Lambda integration processes the TaskToken and passes the results of the waiting execution to continue the workflow execution. 55 | 56 | Tip: Use the payload in the email that is sent to you to simulate the response. Make sure you modify the score before sending it to the Plagiarism API. 57 | 58 | ## Running the demo 59 | 60 | 1. Deploy the backend using AWS SAM CLI 61 | 62 | ```bash 63 | cd src/backend 64 | sam build 65 | sam deploy --stack-name plagiarism --guided 66 | ``` 67 | 68 | **Note:** Make sure you add your emial to the `ToEmail` parameter when prompted. 69 | 70 | 1. Once you have deployed the backend, use the AWS CLI to describe the outputs of the stack to get the API URL 71 | 72 | ```bash 73 | aws cloudformation describe-stacks --stack-name plagiarism --query "Stacks[0].Outputs[*].[OutputKey,OutputValue]" --output table 74 | ``` 75 | 76 | You should see something like this: 77 | 78 | ```bash 79 | --------------------------------------------------------------------------------------------------------------------------------------------------------- 80 | | DescribeStacks | 81 | +--------------------------------------+----------------------------------------------------------------------------------------------------------------+ 82 | | TakeAdministrativeActionFunctionArn | arn:aws:lambda:ap-southeast-2:123456789012:function:plagiarism-TakeAdministrativeActionFunction-wDDCxgR8xEOA | 83 | | ResolveIncidentFunctionArn | arn:aws:lambda:ap-southeast-2:123456789012:function:plagiarism-ResolveIncidentFunction-9JxQ9xDkiSyk | 84 | | ApiEndpointRegisterIncident | https://1a2b3c4d5e.execute-api.ap-southeast-2.amazonaws.com/dev/incident | 85 | | StepFunctionsStateMachine | arn:aws:states:ap-southeast-2:123456789012:stateMachine:PlagiarismStateMachine-dev | 86 | | SubmitExamResultsFunctionArn | arn:aws:lambda:ap-southeast-2:123456789012:function:plagiarism-SubmitExamResultsFunction-fMtI5Ty58sC4 | 87 | | ApiEndpointSubmitExamResults | https://1a2b3c4d5e.execute-api.ap-southeast-2.amazonaws.com/dev/exam | 88 | | ScheduleExamFunctionArn | arn:aws:lambda:ap-southeast-2:123456789012:function:plagiarism-ScheduleExamFunction-KBeqZL1uoinw | 89 | +--------------------------------------+----------------------------------------------------------------------------------------------------------------+ 90 | ``` 91 | 92 | 1. Open the `src/frontend/.env` file in the frontend directory and update the `NEXT_PUBLIC_API_ENDPOINT` parameter with the `ApiEndpointSubmitExamResults` API URL you got from the backend output. 93 | 94 | 1. Now build and run the frontend 95 | 96 | ```bash 97 | cd src/frontend 98 | pnpm install 99 | pnpm dev 100 | ``` 101 | 102 | 1. Open your email client. You should have an email from Amazon SNS to confirm your subscription. Click the **Confirm subscription** link in the email to confirm. 103 | 104 | Email 105 | 106 | 1. Using the `ApiEndpointRegisterIncident` API output from the previous step, register an incident by opening the admin website at http://localhost:3000/admin and clicking the "Register Incident" button. 107 | 108 | Alternively you can use curl to register an incident: 109 | 110 | ```bash 111 | curl --request POST \ 112 | --url https://[YOUR API ID].execute-api.[AWS REGION].amazonaws.com/dev/incident \ 113 | --header 'Content-Type: application/json' \ 114 | --data '{ 115 | "StudentId": "fd794864-e867-435f-a0e1-c4479beafda7", 116 | "IncidentDate": "2024-03-21T19:14:53.418Z" 117 | }' 118 | ``` 119 | 120 | **Tip:** Use [Postman](https://www.postman.com/) or [Insomnia](https://insomnia.rest/) to dynamically alter the `StudentId` and `IncidentDate` values. 121 | 122 | 1. You should receive an email with details about your exam. 123 | 124 | Email 125 | 126 | 1. Click on the top link. This will open the testing centre website, with your incident details. At this stage you should notice that the workflow has paused and is waiting for you to complete the exam. Answer the questions and submit the exam. 127 | 128 | **Tip:** make sure you fail the exam to see the workflow continue. 129 | 130 | ## Resources 131 | 132 | ### Step Functions 133 | 134 | * [AWS Step Functions](https://aws.amazon.com/step-functions/) 135 | * [AWS Step Functions Developer Guide](https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html) 136 | * [Sample projects for Step Functions](https://docs.aws.amazon.com/step-functions/latest/dg/create-sample-projects.html) 137 | * [statelint](https://github.com/awslabs/statelint) 138 | * [Amazon States Language](https://states-language.net/spec.html) 139 | 140 | ### References 141 | 142 | * [AWS Step Functions Examples](https://github.com/aws-samples/aws-stepfunctions-examples) on GitHub 143 | * [AWS Step Functions Supports 200 AWS Services To Enable Easier Workflow Automation](https://aws.amazon.com/blogs/aws/now-aws-step-functions-supports-200-aws-services-to-enable-easier-workflow-automation/) blog post 144 | * [AWS Step Functions Workflow Studio – A Low-Code Visual Tool for Building State Machines](https://aws.amazon.com/blogs/aws/new-aws-step-functions-workflow-studio-a-low-code-visual-tool-for-building-state-machines/) blog post 145 | 146 | ### AWS Developer Resources 147 | 148 | * [Serverless Application Model Developer Guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) 149 | * [AWS::Serverless::StateMachine](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-statemachine.html) 150 | * [AWS Toolkit for Visual Studio Code](https://aws.amazon.com/visualstudiocode/) 151 | 152 | ## License Summary 153 | 154 | This sample code is made available under a modified MIT license. See the LICENSE file. 155 | -------------------------------------------------------------------------------- /amplify.yml: -------------------------------------------------------------------------------- 1 | # This Amplify Console build configuration will deploy the Test Centre website. 2 | # You will need to set an environment variable in Amplify, defining the API 3 | # Gateway endpoint for your Step Functions callback interface. 4 | # The environment variable should be called APIGW_ENDPOINT 5 | # See src/frontend/testcentre/README.md for more details. 6 | version: 0.1 7 | frontend: 8 | phases: 9 | build: 10 | commands: 11 | - cd src/frontend/testcentre 12 | - echo $APIGW_ENDPOINT 13 | - ./replace-endpoint.sh $APIGW_ENDPOINT 14 | - npm install 15 | - npm run build 16 | artifacts: 17 | baseDirectory: src/frontend/testcentre/dist 18 | files: 19 | - '**/*' 20 | cache: 21 | paths: [] 22 | -------------------------------------------------------------------------------- /media/api-step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-step-functions-plagiarism-demo-dotnet/5514aaa30ca5c5cc563f3fbf017de5117c1dae99/media/api-step.png -------------------------------------------------------------------------------- /media/arch-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-step-functions-plagiarism-demo-dotnet/5514aaa30ca5c5cc563f3fbf017de5117c1dae99/media/arch-new.png -------------------------------------------------------------------------------- /media/arch_22_07_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-step-functions-plagiarism-demo-dotnet/5514aaa30ca5c5cc563f3fbf017de5117c1dae99/media/arch_22_07_24.png -------------------------------------------------------------------------------- /media/email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-step-functions-plagiarism-demo-dotnet/5514aaa30ca5c5cc563f3fbf017de5117c1dae99/media/email.png -------------------------------------------------------------------------------- /media/sns_subscription.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-step-functions-plagiarism-demo-dotnet/5514aaa30ca5c5cc563f3fbf017de5117c1dae99/media/sns_subscription.png -------------------------------------------------------------------------------- /media/step_22_07_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-step-functions-plagiarism-demo-dotnet/5514aaa30ca5c5cc563f3fbf017de5117c1dae99/media/step_22_07_24.png -------------------------------------------------------------------------------- /src/backend/.gitignore: -------------------------------------------------------------------------------- 1 | ## Project ignores 2 | artifacts/ 3 | tools/ 4 | publish/ 5 | packaged.yaml 6 | .aws-sam/ 7 | aws-stepfunctions-local-credentials.txt 8 | samconfig.toml 9 | 10 | ## Ignore Visual Studio temporary files, build results, and 11 | ## files generated by popular Visual Studio add-ons. 12 | ## 13 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 14 | 15 | # User-specific files 16 | *.suo 17 | *.user 18 | *.userosscache 19 | *.sln.docstates 20 | 21 | # User-specific files (MonoDevelop/Xamarin Studio) 22 | *.userprefs 23 | 24 | # Build results 25 | [Dd]ebug/ 26 | [Dd]ebugPublic/ 27 | [Rr]elease/ 28 | [Rr]eleases/ 29 | x64/ 30 | x86/ 31 | bld/ 32 | [Bb]in/ 33 | [Oo]bj/ 34 | [Ll]og/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUNIT 49 | *.VisualState.xml 50 | TestResult.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | **/Properties/launchSettings.json 65 | 66 | # StyleCop 67 | StyleCopReport.xml 68 | 69 | # Files built by Visual Studio 70 | *_i.c 71 | *_p.c 72 | *_i.h 73 | *.ilk 74 | *.meta 75 | *.obj 76 | *.iobj 77 | *.pch 78 | *.pdb 79 | *.ipdb 80 | *.pgc 81 | *.pgd 82 | *.rsp 83 | *.sbr 84 | *.tlb 85 | *.tli 86 | *.tlh 87 | *.tmp 88 | *.tmp_proj 89 | *.log 90 | *.vspscc 91 | *.vssscc 92 | .builds 93 | *.pidb 94 | *.svclog 95 | *.scc 96 | 97 | # Chutzpah Test files 98 | _Chutzpah* 99 | 100 | # Visual C++ cache files 101 | ipch/ 102 | *.aps 103 | *.ncb 104 | *.opendb 105 | *.opensdf 106 | *.sdf 107 | *.cachefile 108 | *.VC.db 109 | *.VC.VC.opendb 110 | 111 | # Visual Studio profiler 112 | *.psess 113 | *.vsp 114 | *.vspx 115 | *.sap 116 | 117 | # Visual Studio Trace Files 118 | *.e2e 119 | 120 | # TFS 2012 Local Workspace 121 | $tf/ 122 | 123 | # Guidance Automation Toolkit 124 | *.gpState 125 | 126 | # ReSharper is a .NET coding add-in 127 | _ReSharper*/ 128 | *.[Rr]e[Ss]harper 129 | *.DotSettings.user 130 | 131 | # JustCode is a .NET coding add-in 132 | .JustCode 133 | 134 | # TeamCity is a build add-in 135 | _TeamCity* 136 | 137 | # DotCover is a Code Coverage Tool 138 | *.dotCover 139 | 140 | # AxoCover is a Code Coverage Tool 141 | .axoCover/* 142 | !.axoCover/settings.json 143 | 144 | # Visual Studio code coverage results 145 | *.coverage 146 | *.coveragexml 147 | 148 | # NCrunch 149 | _NCrunch_* 150 | .*crunch*.local.xml 151 | nCrunchTemp_* 152 | 153 | # MightyMoose 154 | *.mm.* 155 | AutoTest.Net/ 156 | 157 | # Web workbench (sass) 158 | .sass-cache/ 159 | 160 | # Installshield output folder 161 | [Ee]xpress/ 162 | 163 | # DocProject is a documentation generator add-in 164 | DocProject/buildhelp/ 165 | DocProject/Help/*.HxT 166 | DocProject/Help/*.HxC 167 | DocProject/Help/*.hhc 168 | DocProject/Help/*.hhk 169 | DocProject/Help/*.hhp 170 | DocProject/Help/Html2 171 | DocProject/Help/html 172 | 173 | # Click-Once directory 174 | publish/ 175 | 176 | # Publish Web Output 177 | *.[Pp]ublish.xml 178 | *.azurePubxml 179 | # Note: Comment the next line if you want to checkin your web deploy settings, 180 | # but database connection strings (with potential passwords) will be unencrypted 181 | *.pubxml 182 | *.publishproj 183 | 184 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 185 | # checkin your Azure Web App publish settings, but sensitive information contained 186 | # in these scripts will be unencrypted 187 | PublishScripts/ 188 | 189 | # NuGet Packages 190 | *.nupkg 191 | # The packages folder can be ignored because of Package Restore 192 | **/[Pp]ackages/* 193 | # except build/, which is used as an MSBuild target. 194 | !**/[Pp]ackages/build/ 195 | # Uncomment if necessary however generally it will be regenerated when needed 196 | #!**/[Pp]ackages/repositories.config 197 | # NuGet v3's project.json files produces more ignorable files 198 | *.nuget.props 199 | *.nuget.targets 200 | 201 | # Microsoft Azure Build Output 202 | csx/ 203 | *.build.csdef 204 | 205 | # Microsoft Azure Emulator 206 | ecf/ 207 | rcf/ 208 | 209 | # Windows Store app package directories and files 210 | AppPackages/ 211 | BundleArtifacts/ 212 | Package.StoreAssociation.xml 213 | _pkginfo.txt 214 | *.appx 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | 265 | # Microsoft Fakes 266 | FakesAssemblies/ 267 | 268 | # GhostDoc plugin setting file 269 | *.GhostDoc.xml 270 | 271 | # Node.js Tools for Visual Studio 272 | .ntvs_analysis.dat 273 | node_modules/ 274 | 275 | # Visual Studio 6 build log 276 | *.plg 277 | 278 | # Visual Studio 6 workspace options file 279 | *.opt 280 | 281 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 282 | *.vbw 283 | 284 | # Visual Studio LightSwitch build output 285 | **/*.HTMLClient/GeneratedArtifacts 286 | **/*.DesktopClient/GeneratedArtifacts 287 | **/*.DesktopClient/ModelManifest.xml 288 | **/*.Server/GeneratedArtifacts 289 | **/*.Server/ModelManifest.xml 290 | _Pvt_Extensions 291 | 292 | # Paket dependency manager 293 | .paket/paket.exe 294 | paket-files/ 295 | 296 | # FAKE - F# Make 297 | .fake/ 298 | 299 | # JetBrains Rider 300 | .idea/ 301 | *.sln.iml 302 | 303 | # CodeRush 304 | .cr/ 305 | 306 | # Python Tools for Visual Studio (PTVS) 307 | __pycache__/ 308 | *.pyc 309 | 310 | # Cake - Uncomment if you are using it 311 | # tools/** 312 | # !tools/packages.config 313 | 314 | # Tabs Studio 315 | *.tss 316 | 317 | # Telerik's JustMock configuration file 318 | *.jmconfig 319 | 320 | # BizTalk build output 321 | *.btp.cs 322 | *.btm.cs 323 | *.odx.cs 324 | *.xsd.cs 325 | 326 | # OpenCover UI analysis results 327 | OpenCover/ 328 | 329 | # Azure Stream Analytics local run output 330 | ASALocalRun/ 331 | 332 | # MSBuild Binary and Structured Log 333 | *.binlog 334 | 335 | # NVidia Nsight GPU debugger configuration file 336 | *.nvuser 337 | 338 | # MFractors (Xamarin productivity tool) working folder 339 | .mfractor/ 340 | 341 | ### JetBrains ### 342 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 343 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 344 | 345 | # User-specific stuff 346 | .idea/**/workspace.xml 347 | .idea/**/tasks.xml 348 | .idea/**/usage.statistics.xml 349 | .idea/**/dictionaries 350 | .idea/**/shelf 351 | 352 | # Sensitive or high-churn files 353 | .idea/**/dataSources/ 354 | .idea/**/dataSources.ids 355 | .idea/**/dataSources.local.xml 356 | .idea/**/sqlDataSources.xml 357 | .idea/**/dynamic.xml 358 | .idea/**/uiDesigner.xml 359 | .idea/**/dbnavigator.xml 360 | 361 | # Gradle 362 | .idea/**/gradle.xml 363 | .idea/**/libraries 364 | 365 | # Gradle and Maven with auto-import 366 | # When using Gradle or Maven with auto-import, you should exclude module files, 367 | # since they will be recreated, and may cause churn. Uncomment if using 368 | # auto-import. 369 | # .idea/modules.xml 370 | # .idea/*.iml 371 | # .idea/modules 372 | 373 | # CMake 374 | cmake-build-*/ 375 | 376 | # Mongo Explorer plugin 377 | .idea/**/mongoSettings.xml 378 | 379 | # File-based project format 380 | *.iws 381 | 382 | # IntelliJ 383 | out/ 384 | 385 | # mpeltonen/sbt-idea plugin 386 | .idea_modules/ 387 | 388 | # JIRA plugin 389 | atlassian-ide-plugin.xml 390 | 391 | # Cursive Clojure plugin 392 | .idea/replstate.xml 393 | 394 | # Crashlytics plugin (for Android Studio and IntelliJ) 395 | com_crashlytics_export_strings.xml 396 | crashlytics.properties 397 | crashlytics-build.properties 398 | fabric.properties 399 | 400 | # Editor-based Rest Client 401 | .idea/httpRequests 402 | 403 | ### JetBrains Patch ### 404 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 405 | 406 | # *.iml 407 | # modules.xml 408 | # .idea/misc.xml 409 | # *.ipr 410 | 411 | # Sonarlint plugin 412 | .idea/sonarlint 413 | 414 | 415 | # End of https://www.gitignore.io/api/jetbrains -------------------------------------------------------------------------------- /src/backend/AWSStepFunctionsPlagiarismDemo.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27703.2026 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdminAction", "AdminAction\AdminAction.csproj", "{8CD9D3A6-4296-4C36-9634-5062D1EA0BF8}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResolveIncident", "ResolveIncident\ResolveIncident.csproj", "{1704A8CC-FF89-4A21-881F-14012E84EA83}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{51287A9C-2976-4934-8962-05B4CC423BDF}" 11 | ProjectSection(SolutionItems) = preProject 12 | README.md = README.md 13 | ..\..\LICENSE = ..\..\LICENSE 14 | EndProjectSection 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScheduleExam", "ScheduleExam\ScheduleExam.csproj", "{A63ABD80-D6BD-47A4-91BE-4C7618581D32}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{296D1A28-AD46-4DD3-866F-2BCAB14A7E4B}" 19 | ProjectSection(SolutionItems) = preProject 20 | template.yaml = template.yaml 21 | api.yaml = api.yaml 22 | state-machine.asl.yaml = state-machine.asl.yaml 23 | EndProjectSection 24 | EndProject 25 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResolveIncident.Tests", "ResolveIncident.Tests\ResolveIncident.Tests.csproj", "{033FDAFD-267E-4BA0-95EB-8EB112D98A03}" 26 | EndProject 27 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SubmitExam", "SubmitExam\SubmitExam.csproj", "{61BDB5E4-6D6E-4284-AB47-4853B747B930}" 28 | EndProject 29 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SubmitExam.Tests", "SubmitExam.Tests\SubmitExam.Tests.csproj", "{31647F12-6E08-4CA8-BFAF-08B5999C2047}" 30 | EndProject 31 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlagiarismRepository", "PlagiarismRepository\PlagiarismRepository.csproj", "{11FF3617-F6BD-4F67-AF05-1BEF3618163C}" 32 | EndProject 33 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plagiarism", "Plagiarism\Plagiarism.csproj", "{E72FF17E-B4A4-4A17-98D6-92C781B4174B}" 34 | EndProject 35 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlagiarismRepository.Tests", "PlagiarismRepository.Tests\PlagiarismRepository.Tests.csproj", "{BA960ECD-5652-454E-BA54-579B36885AD4}" 36 | EndProject 37 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdminAction.Tests", "AdminAction.Tests\AdminAction.Tests.csproj", "{C0051C07-DA4A-4F17-A1C1-84C7C89F541C}" 38 | EndProject 39 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScheduleExam.Tests", "ScheduleExam.Tests\ScheduleExam.Tests.csproj", "{BCBB2382-2365-4F8E-A892-82B516E00B60}" 40 | EndProject 41 | Global 42 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 43 | Debug|Any CPU = Debug|Any CPU 44 | Release|Any CPU = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 47 | {8CD9D3A6-4296-4C36-9634-5062D1EA0BF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {8CD9D3A6-4296-4C36-9634-5062D1EA0BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {8CD9D3A6-4296-4C36-9634-5062D1EA0BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {8CD9D3A6-4296-4C36-9634-5062D1EA0BF8}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {1704A8CC-FF89-4A21-881F-14012E84EA83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {1704A8CC-FF89-4A21-881F-14012E84EA83}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {1704A8CC-FF89-4A21-881F-14012E84EA83}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {1704A8CC-FF89-4A21-881F-14012E84EA83}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {A63ABD80-D6BD-47A4-91BE-4C7618581D32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {A63ABD80-D6BD-47A4-91BE-4C7618581D32}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {A63ABD80-D6BD-47A4-91BE-4C7618581D32}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {A63ABD80-D6BD-47A4-91BE-4C7618581D32}.Release|Any CPU.Build.0 = Release|Any CPU 59 | {033FDAFD-267E-4BA0-95EB-8EB112D98A03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 60 | {033FDAFD-267E-4BA0-95EB-8EB112D98A03}.Debug|Any CPU.Build.0 = Debug|Any CPU 61 | {033FDAFD-267E-4BA0-95EB-8EB112D98A03}.Release|Any CPU.ActiveCfg = Release|Any CPU 62 | {033FDAFD-267E-4BA0-95EB-8EB112D98A03}.Release|Any CPU.Build.0 = Release|Any CPU 63 | {61BDB5E4-6D6E-4284-AB47-4853B747B930}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 64 | {61BDB5E4-6D6E-4284-AB47-4853B747B930}.Debug|Any CPU.Build.0 = Debug|Any CPU 65 | {61BDB5E4-6D6E-4284-AB47-4853B747B930}.Release|Any CPU.ActiveCfg = Release|Any CPU 66 | {61BDB5E4-6D6E-4284-AB47-4853B747B930}.Release|Any CPU.Build.0 = Release|Any CPU 67 | {31647F12-6E08-4CA8-BFAF-08B5999C2047}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 68 | {31647F12-6E08-4CA8-BFAF-08B5999C2047}.Debug|Any CPU.Build.0 = Debug|Any CPU 69 | {31647F12-6E08-4CA8-BFAF-08B5999C2047}.Release|Any CPU.ActiveCfg = Release|Any CPU 70 | {31647F12-6E08-4CA8-BFAF-08B5999C2047}.Release|Any CPU.Build.0 = Release|Any CPU 71 | {11FF3617-F6BD-4F67-AF05-1BEF3618163C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 72 | {11FF3617-F6BD-4F67-AF05-1BEF3618163C}.Debug|Any CPU.Build.0 = Debug|Any CPU 73 | {11FF3617-F6BD-4F67-AF05-1BEF3618163C}.Release|Any CPU.ActiveCfg = Release|Any CPU 74 | {11FF3617-F6BD-4F67-AF05-1BEF3618163C}.Release|Any CPU.Build.0 = Release|Any CPU 75 | {E72FF17E-B4A4-4A17-98D6-92C781B4174B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 76 | {E72FF17E-B4A4-4A17-98D6-92C781B4174B}.Debug|Any CPU.Build.0 = Debug|Any CPU 77 | {E72FF17E-B4A4-4A17-98D6-92C781B4174B}.Release|Any CPU.ActiveCfg = Release|Any CPU 78 | {E72FF17E-B4A4-4A17-98D6-92C781B4174B}.Release|Any CPU.Build.0 = Release|Any CPU 79 | {BA960ECD-5652-454E-BA54-579B36885AD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 80 | {BA960ECD-5652-454E-BA54-579B36885AD4}.Debug|Any CPU.Build.0 = Debug|Any CPU 81 | {BA960ECD-5652-454E-BA54-579B36885AD4}.Release|Any CPU.ActiveCfg = Release|Any CPU 82 | {BA960ECD-5652-454E-BA54-579B36885AD4}.Release|Any CPU.Build.0 = Release|Any CPU 83 | {C0051C07-DA4A-4F17-A1C1-84C7C89F541C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 84 | {C0051C07-DA4A-4F17-A1C1-84C7C89F541C}.Debug|Any CPU.Build.0 = Debug|Any CPU 85 | {C0051C07-DA4A-4F17-A1C1-84C7C89F541C}.Release|Any CPU.ActiveCfg = Release|Any CPU 86 | {C0051C07-DA4A-4F17-A1C1-84C7C89F541C}.Release|Any CPU.Build.0 = Release|Any CPU 87 | {BCBB2382-2365-4F8E-A892-82B516E00B60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 88 | {BCBB2382-2365-4F8E-A892-82B516E00B60}.Debug|Any CPU.Build.0 = Debug|Any CPU 89 | {BCBB2382-2365-4F8E-A892-82B516E00B60}.Release|Any CPU.ActiveCfg = Release|Any CPU 90 | {BCBB2382-2365-4F8E-A892-82B516E00B60}.Release|Any CPU.Build.0 = Release|Any CPU 91 | EndGlobalSection 92 | GlobalSection(SolutionProperties) = preSolution 93 | HideSolutionNode = FALSE 94 | EndGlobalSection 95 | GlobalSection(NestedProjects) = preSolution 96 | EndGlobalSection 97 | GlobalSection(ExtensibilityGlobals) = postSolution 98 | SolutionGuid = {EB157A42-7548-4F1F-885B-DDDF408D3047} 99 | EndGlobalSection 100 | EndGlobal 101 | -------------------------------------------------------------------------------- /src/backend/AdminAction.Tests/AdminAction.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/backend/AdminAction.Tests/FunctionTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | using Xunit; 5 | using Amazon.Lambda.TestUtilities; 6 | using NSubstitute; 7 | using Plagiarism; 8 | using PlagiarismRepository; 9 | using Xunit.Abstractions; 10 | 11 | namespace AdminAction.Tests; 12 | 13 | public class FunctionTests 14 | { 15 | private readonly ITestOutputHelper _testOutputHelper; 16 | 17 | public FunctionTests(ITestOutputHelper testOutputHelper) 18 | { 19 | _testOutputHelper = testOutputHelper; 20 | 21 | // Set env variable for Powertools Metrics 22 | Environment.SetEnvironmentVariable("TABLE_NAME", "IncidentsTable"); 23 | Environment.SetEnvironmentVariable("POWERTOOLS_METRICS_NAMESPACE", "Plagiarism"); 24 | } 25 | 26 | [Fact] 27 | public void ResolveIncidentFunctionTest() 28 | { 29 | var mockIncidentRepository 30 | = Substitute.For(); 31 | 32 | 33 | var function = new Function(mockIncidentRepository); 34 | var context = new TestLambdaContext(); 35 | 36 | var state = new Incident(); 37 | state.IncidentId = Guid.NewGuid(); 38 | state.StudentId = "123"; 39 | state.IncidentDate = new DateTime(2018, 02, 03); 40 | state.Exams = new List() 41 | { 42 | new Exam(Guid.NewGuid(), new DateTime(2018, 02, 10), 10), 43 | new Exam(Guid.NewGuid(), new DateTime(2018, 02, 17), 65) 44 | }; 45 | state.ResolutionDate = null; 46 | 47 | // Call function with mock repository 48 | function.FunctionHandler(state, context); 49 | 50 | // assert the call to incident repository had state with Resolution date not set to null 51 | mockIncidentRepository.Received().SaveIncident(Arg.Is(i => i.ResolutionDate != null)); 52 | mockIncidentRepository.Received().SaveIncident(Arg.Is(i => i.IncidentResolved == false)); 53 | mockIncidentRepository.Received().SaveIncident(Arg.Is(i => i.AdminActionRequired == true)); 54 | 55 | _testOutputHelper.WriteLine("Success"); 56 | } 57 | } -------------------------------------------------------------------------------- /src/backend/AdminAction/AdminAction.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | true 5 | Lambda 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/backend/AdminAction/Function.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | using System; 5 | using Amazon.Lambda.Core; 6 | using AWS.Lambda.Powertools.Logging; 7 | using AWS.Lambda.Powertools.Metrics; 8 | using AWS.Lambda.Powertools.Tracing; 9 | using Plagiarism; 10 | using PlagiarismRepository; 11 | 12 | // Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. 13 | [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] 14 | 15 | namespace AdminAction; 16 | 17 | public class Function 18 | { 19 | private readonly IIncidentRepository _incidentRepository; 20 | 21 | public Function() 22 | { 23 | var tableName = Environment.GetEnvironmentVariable("TABLE_NAME"); 24 | _incidentRepository = new IncidentRepository(tableName); 25 | } 26 | 27 | public Function(IIncidentRepository incidentRepository) 28 | { 29 | _incidentRepository = incidentRepository; 30 | Tracing.RegisterForAllServices(); 31 | } 32 | 33 | /// 34 | /// Indicates that the Incident needs administrative action 35 | /// 36 | /// Instance of Incident class 37 | /// AWS Lambda context 38 | /// 39 | [Logging(LogEvent = true)] 40 | [Tracing(CaptureMode = TracingCaptureMode.ResponseAndError)] 41 | [Metrics(CaptureColdStart = true)] 42 | public void FunctionHandler(Incident incident, ILambdaContext context) 43 | { 44 | incident.AdminActionRequired = true; 45 | incident.IncidentResolved = false; 46 | incident.ResolutionDate = DateTime.Now; 47 | 48 | SaveIncident(incident); 49 | } 50 | 51 | [Tracing(SegmentName = "Save incident")] 52 | protected virtual void SaveIncident(Incident incident) 53 | { 54 | _incidentRepository.SaveIncident(incident); 55 | } 56 | } -------------------------------------------------------------------------------- /src/backend/Plagiarism/ExamNotFoundException.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | using System; 5 | 6 | namespace Plagiarism; 7 | 8 | public class ExamNotFoundException : Exception 9 | { 10 | public ExamNotFoundException() 11 | { 12 | } 13 | 14 | public ExamNotFoundException(string message) : base((string)message) 15 | { 16 | } 17 | } -------------------------------------------------------------------------------- /src/backend/Plagiarism/Incident.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace Plagiarism; 8 | 9 | public class IncidentWrapper 10 | { 11 | public Incident Input { get; set; } 12 | public string TaskToken { get; set; } 13 | } 14 | 15 | public class Incident 16 | { 17 | public Incident() 18 | { 19 | } 20 | 21 | public Incident(string studentId, DateTime incidentDate) 22 | { 23 | StudentId = studentId; 24 | IncidentDate = incidentDate; 25 | IncidentId = Guid.NewGuid(); 26 | Exams = new List(); 27 | IncidentResolved = false; 28 | AdminActionRequired = false; 29 | ResolutionDate = null; 30 | } 31 | 32 | public string StudentId { get; set; } 33 | public Guid IncidentId { get; set; } 34 | public DateTime IncidentDate { get; set; } 35 | public List Exams { get; set; } 36 | public DateTime? ResolutionDate { get; set; } 37 | public bool IncidentResolved { get; set; } 38 | public bool AdminActionRequired { get; set; } 39 | } 40 | 41 | public class Exam 42 | { 43 | public Exam() 44 | { 45 | 46 | } 47 | 48 | private ExamResult _examResult; 49 | public Guid ExamId { get; set; } 50 | public DateTime ExamDeadline { get; set; } 51 | public int Score { get; set; } 52 | 53 | public ExamResult Result 54 | { 55 | get 56 | { 57 | if (Score >= 76) 58 | { 59 | return _examResult = ExamResult.Pass; 60 | } 61 | 62 | if (Score >= 1 & Score < 76) 63 | { 64 | return _examResult = ExamResult.Fail; 65 | } 66 | 67 | return _examResult = ExamResult.DidNotSitExam; 68 | } 69 | 70 | set => _examResult = value; 71 | } 72 | 73 | public bool NotificationSent { get; set; } 74 | 75 | public Exam(Guid examId, DateTime examDeadline, int score) 76 | { 77 | ExamId = examId; 78 | ExamDeadline = examDeadline; 79 | Score = score; 80 | NotificationSent = false; 81 | } 82 | 83 | public override string ToString() 84 | { 85 | return 86 | $"ExamId: {ExamId}, ExamDate: {ExamDeadline}, Score: {Score}, Result: {Result}, NotificationSent: {NotificationSent}"; 87 | } 88 | } 89 | 90 | public enum ExamResult 91 | { 92 | Pass, 93 | Fail, 94 | DidNotSitExam 95 | } -------------------------------------------------------------------------------- /src/backend/Plagiarism/IncidentNotFoundException.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | using System; 5 | 6 | namespace Plagiarism; 7 | 8 | public class IncidentNotFoundException : Exception 9 | { 10 | public IncidentNotFoundException() 11 | { 12 | } 13 | 14 | public IncidentNotFoundException(string message) : base((string)message) 15 | { 16 | } 17 | } -------------------------------------------------------------------------------- /src/backend/Plagiarism/Plagiarism.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/backend/Plagiarism/StudentExceededAllowableExamRetries.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | using System; 5 | 6 | namespace Plagiarism; 7 | 8 | /// 9 | /// Custom Exception for students that have exceeded the allowable number of exam retries. 10 | /// 11 | /// 12 | public class StudentExceededAllowableExamRetries : Exception 13 | { 14 | public StudentExceededAllowableExamRetries() 15 | { 16 | } 17 | 18 | public StudentExceededAllowableExamRetries(string message) : base((string)message) 19 | { 20 | } 21 | } -------------------------------------------------------------------------------- /src/backend/PlagiarismRepository.Tests/PlagiarismRepository.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/backend/PlagiarismRepository.Tests/RepositoryTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Amazon.DynamoDBv2; 9 | using Amazon.DynamoDBv2.Model; 10 | using Plagiarism; 11 | using Xunit; 12 | using Xunit.Abstractions; 13 | 14 | namespace PlagiarismRepository.Tests; 15 | 16 | public class RepositoryTests 17 | { 18 | private readonly ITestOutputHelper _testOutputHelper; 19 | private const string TablePrefix = "IncidentsTestTable-"; 20 | private string _tableName; 21 | private readonly AmazonDynamoDBClient _dynamoDbClient; 22 | private IIncidentRepository _incidentRepository; 23 | 24 | 25 | public RepositoryTests(ITestOutputHelper testOutputHelper) 26 | { 27 | _testOutputHelper = testOutputHelper; 28 | 29 | // dynamodb client using DynamoDB local 30 | var dynamoDbConfig = new AmazonDynamoDBConfig 31 | { 32 | ServiceURL = "http://localhost:8000", 33 | Timeout = new TimeSpan(0,0,5), 34 | }; 35 | _dynamoDbClient = new AmazonDynamoDBClient(dynamoDbConfig); 36 | EnsureDockerContainerRunning(); 37 | SetupTableAsync().Wait(); 38 | } 39 | 40 | 41 | private void EnsureDockerContainerRunning() 42 | { 43 | var retryCount = 0; 44 | const int maxRetries = 3; // Reduced from 5 45 | const int retryDelayMs = 1000; // Reduced from 2000 46 | 47 | while (retryCount < maxRetries) 48 | { 49 | try 50 | { 51 | // Attempt to list tables to check if DynamoDB is responsive 52 | _dynamoDbClient.ListTablesAsync().Wait(TimeSpan.FromSeconds(2)); // Added timeout 53 | _testOutputHelper.WriteLine("DynamoDB Local container is running."); 54 | return; 55 | } 56 | catch (Exception ex) 57 | { 58 | _testOutputHelper.WriteLine($"Attempt {retryCount + 1}: DynamoDB Local container is not ready. Retrying in {retryDelayMs}ms..."); 59 | _testOutputHelper.WriteLine($"Error: {ex.Message}"); 60 | Thread.Sleep(retryDelayMs); 61 | retryCount++; 62 | } 63 | } 64 | 65 | throw new Exception("Failed to connect to DynamoDB Local after multiple attempts. Ensure the Docker container is running."); 66 | 67 | } 68 | 69 | [Fact] 70 | public void SaveIncidentAsync() 71 | { 72 | _incidentRepository = new IncidentRepository(_dynamoDbClient, _tableName); 73 | 74 | var newIncident = new Incident 75 | { 76 | IncidentId = Guid.NewGuid(), 77 | StudentId = "123", 78 | IncidentDate = new DateTime(2018, 02, 03), 79 | ResolutionDate = null 80 | }; 81 | 82 | var incident = _incidentRepository.SaveIncident(newIncident); 83 | 84 | Assert.NotNull(incident); 85 | } 86 | 87 | [Fact] 88 | public void UpdateIncidentAsync() 89 | { 90 | _incidentRepository = new IncidentRepository(_dynamoDbClient, _tableName); 91 | 92 | var state = new Incident 93 | { 94 | IncidentId = Guid.NewGuid(), 95 | StudentId = "123", 96 | IncidentDate = new DateTime(2018, 02, 03), 97 | ResolutionDate = null 98 | }; 99 | // state.Exams = new List 100 | // { 101 | // new(Guid.NewGuid(), new DateTime(2018, 02, 17), 0), 102 | // new(Guid.NewGuid(), new DateTime(2018, 02, 10), 65) 103 | // }; 104 | 105 | var incident = _incidentRepository.SaveIncident(state); 106 | 107 | //incident.Exams.Add(new(Guid.NewGuid(), new DateTime(2018, 02, 17), 99)); 108 | 109 | var updatedIncident = _incidentRepository.SaveIncident(state); 110 | 111 | Assert.NotNull(incident); 112 | Assert.NotNull(updatedIncident); 113 | // Assert.True(updatedIncident.Exams.Count == 3, "Should be three"); 114 | } 115 | 116 | 117 | [Fact] 118 | public void FindIncidentAsync() 119 | { 120 | _incidentRepository = new IncidentRepository(_dynamoDbClient, _tableName); 121 | 122 | var incidentDate = DateTime.Now; 123 | 124 | var state = new Incident 125 | { 126 | IncidentId = Guid.NewGuid(), 127 | StudentId = "123", 128 | IncidentDate = incidentDate, 129 | ResolutionDate = DateTime.Now 130 | }; 131 | 132 | var incident = _incidentRepository.SaveIncident(state); 133 | 134 | var newIncident = _incidentRepository.GetIncidentById(incident.IncidentId); 135 | 136 | Assert.NotNull(newIncident); 137 | Assert.True(newIncident.IncidentId == incident.IncidentId, "Should be the same incident"); 138 | } 139 | 140 | /// 141 | /// Helper function to create a testing table 142 | /// 143 | /// 144 | private async Task SetupTableAsync() 145 | { 146 | var listTablesResponse = await _dynamoDbClient.ListTablesAsync(new ListTablesRequest()); 147 | 148 | var existingTestTable = 149 | listTablesResponse.TableNames.FindAll(s => s.StartsWith(TablePrefix)).FirstOrDefault(); 150 | 151 | if (existingTestTable == null) 152 | { 153 | _tableName = TablePrefix + DateTime.Now.Ticks; 154 | 155 | var request = new CreateTableRequest 156 | { 157 | TableName = _tableName, 158 | ProvisionedThroughput = new ProvisionedThroughput 159 | { 160 | ReadCapacityUnits = 2, 161 | WriteCapacityUnits = 2 162 | }, 163 | KeySchema = 164 | [ 165 | new KeySchemaElement 166 | { 167 | AttributeName = "IncidentId", 168 | KeyType = KeyType.HASH 169 | } 170 | ], 171 | AttributeDefinitions = 172 | [ 173 | new AttributeDefinition 174 | { 175 | AttributeName = "IncidentId", 176 | AttributeType = ScalarAttributeType.S 177 | } 178 | ] 179 | }; 180 | 181 | try 182 | { 183 | await _dynamoDbClient.CreateTableAsync(request); 184 | } 185 | catch (Exception e) 186 | { 187 | _testOutputHelper.WriteLine(e.Message); 188 | throw; 189 | } 190 | 191 | var describeRequest = new DescribeTableRequest { TableName = _tableName }; 192 | DescribeTableResponse response; 193 | 194 | do 195 | { 196 | Thread.Sleep(1000); 197 | response = await _dynamoDbClient.DescribeTableAsync(describeRequest); 198 | } while (response.Table.TableStatus != TableStatus.ACTIVE); 199 | } 200 | else 201 | { 202 | _testOutputHelper.WriteLine($"Using existing test table {existingTestTable}"); 203 | _tableName = existingTestTable; 204 | } 205 | } 206 | } -------------------------------------------------------------------------------- /src/backend/PlagiarismRepository.Tests/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | services: 4 | dynamo: 5 | container_name: local-dynamodb 6 | image: amazon/dynamodb-local 7 | networks: 8 | - local-dynamodb 9 | ports: 10 | - "8000:8000" 11 | volumes: 12 | - dynamodata:/home/dynamodblocal 13 | working_dir: /home/dynamodblocal 14 | command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ." 15 | 16 | networks: 17 | local-dynamodb: 18 | name: local-dynamodb 19 | 20 | volumes: 21 | dynamodata: { } 22 | -------------------------------------------------------------------------------- /src/backend/PlagiarismRepository/IIncidentRepository.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | using System; 5 | using Plagiarism; 6 | 7 | namespace PlagiarismRepository; 8 | 9 | public interface IIncidentRepository 10 | { 11 | /// 12 | /// Saves incident 13 | /// 14 | /// Incident instance 15 | /// Incident instance saved to the table 16 | Incident SaveIncident(Incident incident); 17 | 18 | /// 19 | /// Gets incident by id 20 | /// 21 | /// Incident Id 22 | /// Incident instance 23 | Incident GetIncidentById(Guid incidentId); 24 | } -------------------------------------------------------------------------------- /src/backend/PlagiarismRepository/IncidentRepository.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | using System; 5 | using Amazon; 6 | using Amazon.DynamoDBv2; 7 | using Amazon.DynamoDBv2.DataModel; 8 | using Plagiarism; 9 | using AWS.Lambda.Powertools.Logging; 10 | 11 | namespace PlagiarismRepository; 12 | 13 | public class IncidentRepository : IIncidentRepository 14 | { 15 | private readonly DynamoDBContext _dynamoDbContext; 16 | private readonly string _tableName; 17 | 18 | /// 19 | /// Constructor 20 | /// 21 | /// DynamoDb table name 22 | public IncidentRepository(string tableName) 23 | { 24 | if (!string.IsNullOrEmpty(tableName)) 25 | { 26 | _tableName = tableName; 27 | AWSConfigsDynamoDB.Context.TypeMappings[typeof(Incident)] = 28 | new Amazon.Util.TypeMapping(typeof(Incident), tableName); 29 | } 30 | 31 | var config = new DynamoDBContextConfig 32 | { 33 | Conversion = DynamoDBEntryConversion.V2 34 | }; 35 | 36 | _dynamoDbContext = new DynamoDBContext(new AmazonDynamoDBClient(), config); 37 | } 38 | 39 | /// 40 | /// Constructor used for testing passing in a preconfigured DynamoDB client. 41 | /// 42 | /// 43 | /// 44 | public IncidentRepository(AmazonDynamoDBClient ddbClient, string tableName) 45 | { 46 | if (!string.IsNullOrEmpty(tableName)) 47 | { 48 | _tableName = tableName; 49 | AWSConfigsDynamoDB.Context.TypeMappings[typeof(Incident)] = 50 | new Amazon.Util.TypeMapping(typeof(Incident), tableName); 51 | } 52 | 53 | var config = new DynamoDBContextConfig 54 | { 55 | Conversion = DynamoDBEntryConversion.V2 56 | }; 57 | _dynamoDbContext = new DynamoDBContext(ddbClient, config); 58 | } 59 | 60 | public Incident GetIncidentById(Guid incidentId) 61 | { 62 | Logger.LogInformation("Getting {incidentId}", incidentId); 63 | var incident = _dynamoDbContext.LoadAsync(incidentId).Result; 64 | Logger.LogInformation($"Found Incident: {incident != null}"); 65 | 66 | if (incident == null) 67 | { 68 | throw new IncidentNotFoundException($"Could not locate {incidentId} in table {_tableName}"); 69 | } 70 | 71 | return incident; 72 | } 73 | 74 | /// 75 | /// 76 | /// 77 | /// 78 | /// Instance of State 79 | public Incident SaveIncident(Incident incident) 80 | { 81 | try 82 | { 83 | Logger.LogInformation($"Saving incident with id {incident.IncidentId}"); 84 | 85 | _dynamoDbContext.SaveAsync(incident).Wait(); 86 | return incident; 87 | } 88 | catch (AmazonDynamoDBException e) 89 | { 90 | Logger.LogInformation(e); 91 | throw; 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /src/backend/PlagiarismRepository/PlagiarismRepository.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/backend/README.md: -------------------------------------------------------------------------------- 1 | # Developing with AWS Step Functions using .NET 8 2 | 3 | ## Requirements 4 | 5 | The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality 6 | for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that 7 | matches Lambda. 8 | 9 | To use the SAM CLI, you need the following tools: 10 | 11 | * [AWS CLI](https://aws.amazon.com/cli/) configured with Administrator permission 12 | * SAM 13 | CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 14 | * .NET 8 - [Install .NET 8](https://www.microsoft.com/net/download) 15 | * Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) 16 | 17 | Please see 18 | the [currently supported patch of each major version of .NET Core](https://github.com/aws/aws-lambda-dotnet#version-status) 19 | to ensure your functions are compatible with the AWS Lambda runtime. 20 | 21 | ## Building your application 22 | 23 | To build and deploy your application for the first time, run the following in your shell: 24 | 25 | ```bash 26 | sam build 27 | sam deploy --guided 28 | ``` 29 | 30 | The first command will build the source of your application. The second command will package and deploy your application 31 | to AWS, with a series of prompts: 32 | 33 | * **Stack Name**: The name of the stack to deploy to CloudFormation. This should be unique to your account and region, 34 | and a good starting point would be something matching your project name. 35 | * **AWS Region**: The AWS region you want to deploy your app to. 36 | * **Confirm changes before deploy**: If set to yes, any change sets will be shown to you before execution for manual 37 | review. If set to no, the AWS SAM CLI will automatically deploy application changes. 38 | * **Allow SAM CLI IAM role creation**: Many AWS SAM templates, including this example, create AWS IAM roles required for 39 | the AWS Lambda function(s) included to access AWS services. By default, these are scoped down to minimum required 40 | permissions. To deploy an AWS CloudFormation stack which creates or modified IAM roles, the `CAPABILITY_IAM` value 41 | for `capabilities` must be provided. If permission isn't provided through this prompt, to deploy this example you must 42 | explicitly pass `--capabilities CAPABILITY_IAM` to the `sam deploy` command. 43 | * **Save arguments to samconfig.toml**: If set to yes, your choices will be saved to a configuration file inside the 44 | project, so that in the future you can just re-run `sam deploy` without parameters to deploy changes to your 45 | application. 46 | 47 | You can find your API Gateway Endpoint URL in the output values displayed after deployment. 48 | 49 | ## Use the SAM CLI to build and test locally 50 | 51 | Build the Lambda functions in your application with the `sam build` command. 52 | 53 | ```bash 54 | plagiarism-demo$ sam build 55 | ``` 56 | 57 | The SAM CLI installs dependencies defined in `functions/FunctionName.csproj`, creates a deployment package, and saves it 58 | in the `.aws-sam/build` folder. 59 | 60 | ## Add a resource to your application 61 | 62 | The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an 63 | extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as 64 | functions, triggers, and APIs. For resources not included 65 | in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), 66 | you can use 67 | standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) 68 | resource types. 69 | 70 | ## Fetch, tail, and filter Lambda function logs 71 | 72 | To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your 73 | deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has 74 | several nifty features to help you quickly find the bug. 75 | 76 | `NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. 77 | 78 | ```bash 79 | plagiarism-demo$ sam logs -n RegisterIncidentFunction --stack-name plagiarism-demo --tail 80 | ``` 81 | 82 | You can find more information and examples about filtering Lambda function logs in 83 | the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). 84 | 85 | ## Cleanup 86 | 87 | To delete the sample application that you created, use the AWS CLI. Assuming you used your project name for the stack 88 | name, you can run the following: 89 | 90 | ```bash 91 | aws cloudformation delete-stack --stack-name plagiarism-demo 92 | ``` 93 | 94 | ## Resources 95 | 96 | See 97 | the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) 98 | for an introduction to SAM specification, the SAM CLI, and serverless application concepts. 99 | 100 | Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples 101 | and learn how authors developed their 102 | applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) 103 | 104 | ## Next Steps 105 | 106 | Create your own .NET Core solution template to use with SAM 107 | CLI. [Cookiecutter for AWS SAM and .NET](https://github.com/aws-samples/cookiecutter-aws-sam-dotnet) provides you with a 108 | sample implementation how to use cookiecutter templating library to standardise how you initialise your Serverless 109 | projects. 110 | 111 | ``` bash 112 | sam init --location gh:aws-samples/cookiecutter-aws-sam-dotnet 113 | ``` 114 | 115 | For more information and examples of how to use `sam init` run 116 | 117 | ``` bash 118 | sam init --help 119 | ``` 120 | -------------------------------------------------------------------------------- /src/backend/ResolveIncident.Tests/FunctionTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using Xunit; 7 | using Amazon.Lambda.TestUtilities; 8 | using NSubstitute; 9 | using Plagiarism; 10 | using PlagiarismRepository; 11 | using Xunit.Abstractions; 12 | 13 | 14 | namespace ResolveIncident.Tests; 15 | 16 | public class FunctionTests 17 | { 18 | private readonly ITestOutputHelper _testOutputHelper; 19 | 20 | public FunctionTests(ITestOutputHelper testOutputHelper) 21 | { 22 | _testOutputHelper = testOutputHelper; 23 | 24 | // Set env variable for Powertools Metrics 25 | Environment.SetEnvironmentVariable("TABLE_NAME", "IncidentsTable"); 26 | Environment.SetEnvironmentVariable("POWERTOOLS_METRICS_NAMESPACE", "Plagiarism"); 27 | } 28 | 29 | 30 | [Fact] 31 | public void ResolveIncidentFunctionTest() 32 | { 33 | var mockIncidentRepository 34 | = Substitute.For(); 35 | 36 | 37 | var function = new Function(mockIncidentRepository); 38 | var context = new TestLambdaContext(); 39 | 40 | var state = new Incident(); 41 | state.IncidentId = Guid.NewGuid(); 42 | state.StudentId = "123"; 43 | state.IncidentDate = new DateTime(2018, 02, 03); 44 | state.Exams = new List() 45 | { 46 | new Exam(Guid.NewGuid(), new DateTime(2018, 02, 10), 10), 47 | new Exam(Guid.NewGuid(), new DateTime(2018, 02, 17), 65) 48 | }; 49 | state.ResolutionDate = null; 50 | 51 | // Call function with mock repository 52 | function.FunctionHandler(state, context); 53 | 54 | // assert the call to incident repository had state with Resolution date not set to null 55 | mockIncidentRepository.Received().SaveIncident(Arg.Is(i => i.ResolutionDate != null)); 56 | mockIncidentRepository.Received().SaveIncident(Arg.Is(i => i.IncidentResolved == true)); 57 | } 58 | } -------------------------------------------------------------------------------- /src/backend/ResolveIncident.Tests/ResolveIncident.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/backend/ResolveIncident/Function.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | using System; 5 | using Amazon.Lambda.Core; 6 | using Plagiarism; 7 | using PlagiarismRepository; 8 | using AWS.Lambda.Powertools.Logging; 9 | using AWS.Lambda.Powertools.Metrics; 10 | using AWS.Lambda.Powertools.Tracing; 11 | 12 | // Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. 13 | [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] 14 | 15 | namespace ResolveIncident; 16 | 17 | public class Function 18 | { 19 | private readonly IIncidentRepository _incidentRepository; 20 | 21 | /// 22 | /// Constructs a new instance with the specified repository. 23 | /// 24 | /// 25 | /// The repository parameter cannot be null. 26 | /// 27 | public Function() 28 | { 29 | Tracing.RegisterForAllServices(); 30 | _incidentRepository = new IncidentRepository(Environment.GetEnvironmentVariable("TABLE_NAME")); 31 | } 32 | 33 | /// 34 | /// Constructor used for testing purposes 35 | /// 36 | /// 37 | public Function(IIncidentRepository repository) 38 | { 39 | if (repository == null) 40 | { 41 | throw new ArgumentNullException(nameof(repository)); 42 | } 43 | 44 | Tracing.RegisterForAllServices(); 45 | _incidentRepository = repository; 46 | } 47 | 48 | /// 49 | /// Function to resolve the incident and complete the workflow. 50 | /// All state data is persisted. 51 | /// 52 | /// Instance of an Incident 53 | /// AWS Lambda context 54 | /// 55 | [Logging(LogEvent = true)] 56 | [Tracing(CaptureMode = TracingCaptureMode.ResponseAndError)] 57 | [Metrics(CaptureColdStart = true)] 58 | public void FunctionHandler(Incident incident, ILambdaContext context) 59 | { 60 | SaveIncident(incident); 61 | } 62 | 63 | [Tracing(SegmentName = "Saving incident")] 64 | private void SaveIncident(Incident incident) 65 | { 66 | incident.AdminActionRequired = false; 67 | incident.IncidentResolved = true; 68 | incident.ResolutionDate = DateTime.Now; 69 | 70 | _incidentRepository.SaveIncident(incident); 71 | } 72 | } -------------------------------------------------------------------------------- /src/backend/ResolveIncident/ResolveIncident.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | true 5 | Lambda 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/backend/ScheduleExam.Tests/FunctionTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | using NSubstitute; 3 | using Plagiarism; 4 | using PlagiarismRepository; 5 | using ScheduleExam; 6 | 7 | namespace ScheduleExam.Tests 8 | { 9 | public class FunctionTests 10 | { 11 | private readonly IIncidentRepository _repository; 12 | private readonly Function _function; 13 | 14 | public FunctionTests() 15 | { 16 | // Set env variable for Powertools Metrics 17 | Environment.SetEnvironmentVariable("TABLE_NAME", "IncidentsTable"); 18 | Environment.SetEnvironmentVariable("POWERTOOLS_METRICS_NAMESPACE", "Plagiarism"); 19 | _repository = Substitute.For(); 20 | _function = new Function(_repository); 21 | } 22 | 23 | [Fact] 24 | public void FunctionHandler_ValidIncident_SchedulesExam() 25 | { 26 | // Arrange 27 | var incidentId = Guid.NewGuid(); 28 | var incident = new Incident { IncidentId = incidentId }; 29 | var existingIncident = new Incident { IncidentId = incidentId, Exams = new List() }; 30 | _repository.GetIncidentById(incident.IncidentId).Returns(existingIncident); 31 | 32 | // Act 33 | var result = _function.FunctionHandler(incident, null); 34 | 35 | // Assert 36 | Assert.Single(result.Exams); 37 | Assert.Equal(DateTime.Now.Date.AddDays(7), result.Exams[0].ExamDeadline.Date); 38 | _repository.Received(1).SaveIncident(Arg.Any()); 39 | } 40 | 41 | [Fact] 42 | public void FunctionHandler_IncidentNotFound_ThrowsException() 43 | { 44 | // Arrange 45 | var incidentId = Guid.NewGuid(); 46 | var incident = new Incident { IncidentId = incidentId }; 47 | _repository.GetIncidentById(incident.IncidentId).Returns((Incident)null); 48 | 49 | // Act & Assert 50 | Assert.Throws(() => _function.FunctionHandler(incident, null)); 51 | } 52 | 53 | [Fact] 54 | public void FunctionHandler_ThreeExamsAlreadyTaken_ThrowsException() 55 | { 56 | // Arrange 57 | var incidentId = Guid.NewGuid(); 58 | var incident = new Incident { IncidentId = incidentId }; 59 | var existingIncident = new Incident 60 | { 61 | IncidentId = incidentId, 62 | Exams = new List { new Exam(), new Exam(), new Exam() } 63 | }; 64 | _repository.GetIncidentById(incident.IncidentId).Returns(existingIncident); 65 | 66 | // Act & Assert 67 | Assert.Throws(() => _function.FunctionHandler(incident, null)); 68 | } 69 | 70 | [Fact] 71 | public void FunctionHandler_NullIncident_ThrowsArgumentNullException() 72 | { 73 | // Act & Assert 74 | Assert.Throws(() => _function.FunctionHandler(null, null)); 75 | } 76 | 77 | [Fact] 78 | public void FunctionHandler_EmptyIncidentId_ThrowsArgumentException() 79 | { 80 | // Arrange 81 | var incident = new Incident { IncidentId = Guid.Empty }; 82 | 83 | // Act & Assert 84 | Assert.Throws(() => _function.FunctionHandler(incident, null)); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /src/backend/ScheduleExam.Tests/ScheduleExam.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/backend/ScheduleExam/Function.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using Amazon.DynamoDBv2; 7 | using Amazon.Lambda.Core; 8 | using AWS.Lambda.Powertools.Logging; 9 | using AWS.Lambda.Powertools.Metrics; 10 | using AWS.Lambda.Powertools.Tracing; 11 | using Plagiarism; 12 | using PlagiarismRepository; 13 | 14 | // Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. 15 | [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] 16 | 17 | namespace ScheduleExam; 18 | 19 | public class Function 20 | { 21 | private readonly IIncidentRepository _incidentRepository; 22 | 23 | public Function() : this(new IncidentRepository(Environment.GetEnvironmentVariable("TABLE_NAME"))) 24 | { 25 | } 26 | 27 | public Function(AmazonDynamoDBClient ddbClient, string tableName) 28 | : this(new IncidentRepository(ddbClient, tableName)) 29 | { 30 | } 31 | 32 | public Function(IIncidentRepository incidentRepository) 33 | { 34 | Tracing.RegisterForAllServices(); 35 | _incidentRepository = incidentRepository; 36 | } 37 | 38 | [Logging(LogEvent = true)] 39 | [Tracing(CaptureMode = TracingCaptureMode.ResponseAndError)] 40 | [Metrics(CaptureColdStart = true)] 41 | public Incident FunctionHandler(Incident incident, ILambdaContext context) 42 | { 43 | ValidateIncident(incident); 44 | 45 | var existingIncident = _incidentRepository.GetIncidentById(incident.IncidentId) 46 | ?? throw new InvalidOperationException($"Incident with ID {incident.IncidentId} not found."); 47 | 48 | Logger.LogInformation("Scheduling exam for incident {IncidentId}", existingIncident.IncidentId); 49 | 50 | if (existingIncident.Exams?.Count >= 3) 51 | { 52 | Logger.LogInformation("Student has already completed 3 exams for incident {IncidentId}", existingIncident.IncidentId); 53 | throw new StudentExceededAllowableExamRetries("Student cannot take more than 3 exams."); 54 | } 55 | 56 | existingIncident.Exams ??= new List(); 57 | existingIncident.Exams.Insert(0, new Exam(Guid.NewGuid(), DateTime.Now.AddDays(7), 0)); 58 | 59 | _incidentRepository.SaveIncident(existingIncident); 60 | Logger.LogInformation("Exam for incident {IncidentId} scheduled.", existingIncident.IncidentId); 61 | return existingIncident; 62 | } 63 | 64 | private static void ValidateIncident(Incident incident) 65 | { 66 | if (incident == null) 67 | { 68 | throw new ArgumentNullException(nameof(incident), "Incident cannot be null."); 69 | } 70 | 71 | if (incident.IncidentId == Guid.Empty) 72 | { 73 | throw new ArgumentException("IncidentId cannot be null or empty.", nameof(incident)); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/backend/ScheduleExam/ScheduleExam.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | true 5 | Lambda 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/backend/SubmitExam.Tests/FunctionTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading; 7 | using Amazon.Lambda.APIGatewayEvents; 8 | using Amazon.Lambda.TestUtilities; 9 | using Amazon.StepFunctions; 10 | using Amazon.StepFunctions.Model; 11 | using Newtonsoft.Json; 12 | using NSubstitute; 13 | using Plagiarism; 14 | using Xunit; 15 | using Xunit.Abstractions; 16 | using PlagiarismRepository; 17 | 18 | namespace SubmitExam.Tests; 19 | 20 | public class FunctionTests 21 | { 22 | private readonly ITestOutputHelper _testOutputHelper; 23 | 24 | private const string Token = 25 | "AAAAKgAAAAIAAAAAAAAAAbdvA5UnsPbXk2HGkayUMygJK8eFJq3pnwBV/xTTDwiIbXvk246zL6Y1+UxXRWzPnbLD0mex2AEUEwMfjxjOj0lW0" + 26 | "g+6AwFv6gA0MW/gU2SAdkHZl7tQQ1o3uBL2eOlSSYakcvPvF35BJdXCFkhhKaoqB8CzpnzkJPr7KVSXumjMouy/C4KwJJMqcVpeIW2Xhjyxq6F" + 27 | "FT8+GRfNspJUaGE3aId15q/dK94xRTPG/Gidez7iuINk6Y7JpbA4/sj3T2hpUuDKyi4CcCkI8A4z93Hn2Tw2OMqWwhmserDGNfI3UgW3Um6pHR" + 28 | "YNvL1prARZ9DkGHHftGaaXXBU8IO1mxYij4TciyP2Cky4b/Dk6ImioM0s+xdIeFOfMprMg73KG5WPK0XAWF+coMC7zBKJTtHZmudk9wKzTPdiS" + 29 | "EZrwmPgeD3hVeWTQXwi7GF9hVbpS8wz/QrtI78HGPcbUdMi0Y79YihuGDo6iN4booO/5Tek3prcfDKhU3JtqqqVFRp9ugqQlOxhnkGmKaajp5mi" + 30 | "RFDcgrghxvP8Fp4D1DDY+/5vUxHFS+tOqvrp24YpSfO51xQxp7GWeg0k9qSnSWntOKdJRjmE7gyvIhKC9XMnlLktJEeBpCQa/B3pqzIr31sPB9ooDTS7m97REIl6Gf0VOtOx4="; 31 | 32 | public FunctionTests(ITestOutputHelper testOutputHelper) 33 | { 34 | // Set env variable for Powertools Metrics 35 | Environment.SetEnvironmentVariable("TABLE_NAME", "IncidentsTable"); 36 | Environment.SetEnvironmentVariable("POWERTOOLS_METRICS_NAMESPACE", "Plagiarism"); 37 | _testOutputHelper = testOutputHelper; 38 | } 39 | 40 | 41 | [Fact] 42 | public void SubmitExamHandlerReturns200ForValidRequest() 43 | { 44 | var mockStepFunctionsClient = Substitute.ForPartsOf(); 45 | var mockIncidentRepository = Substitute.For(); 46 | 47 | var incidentId = Guid.NewGuid(); 48 | var examId = Guid.NewGuid(); 49 | var request = new APIGatewayProxyRequest 50 | { 51 | Body = JsonConvert.SerializeObject(new Dictionary 52 | { 53 | { "IncidentId", incidentId.ToString() }, 54 | { "ExamId", examId.ToString() }, 55 | { "Score", "99" }, 56 | { "TaskToken", Token } 57 | }) 58 | }; 59 | 60 | var context = new TestLambdaContext(); 61 | 62 | var expectedResponse = new APIGatewayProxyResponse 63 | { 64 | StatusCode = 200, 65 | Headers = new Dictionary 66 | { 67 | { "Content-Type", "application/json" }, 68 | { "Access-Control-Allow-Origin", "*" }, 69 | { "Access-Control-Allow-Headers", "Content-Type,X-Requested-With,Accept" }, 70 | { "Access-Control-Allow-Methods", "OPTIONS,POST" } 71 | } 72 | }; 73 | 74 | // create a new incident with one exam 75 | var existingIncident = new Incident(); 76 | existingIncident.IncidentId = incidentId; 77 | existingIncident.IncidentDate = DateTime.UtcNow; 78 | existingIncident.StudentId = Guid.NewGuid().ToString(); 79 | existingIncident.Exams = new List 80 | { 81 | new Exam(examId, DateTime.Now, 0) 82 | }; 83 | 84 | var savedIncident = new Incident(); 85 | savedIncident.IncidentId = incidentId; 86 | savedIncident.IncidentDate = DateTime.UtcNow; 87 | savedIncident.StudentId = Guid.NewGuid().ToString(); 88 | savedIncident.Exams = new List 89 | { 90 | new Exam(examId, DateTime.Now, 99) 91 | }; 92 | 93 | mockIncidentRepository.GetIncidentById(incidentId).Returns(info => 94 | existingIncident 95 | ); 96 | 97 | mockIncidentRepository.SaveIncident(Arg.Any()).Returns(info => 98 | savedIncident 99 | ); 100 | 101 | mockStepFunctionsClient.SendTaskSuccessAsync(Arg.Any(), CancellationToken.None) 102 | .Returns(new SendTaskSuccessResponse()); 103 | 104 | var function = new Function(mockStepFunctionsClient, mockIncidentRepository); 105 | var response = function.FunctionHandler(request, context); 106 | 107 | _testOutputHelper.WriteLine("Lambda Response: \n" + response.StatusCode); 108 | _testOutputHelper.WriteLine("Expected Response: \n" + expectedResponse.StatusCode); 109 | 110 | Assert.Equal(expectedResponse.Body, response.Body); 111 | Assert.Equal(expectedResponse.Headers, response.Headers); 112 | Assert.Equal(expectedResponse.StatusCode, response.StatusCode); 113 | } 114 | 115 | [Fact] 116 | public void SubmitExamHandlerReturns400ForInvalidExamIdRequest() 117 | { 118 | var mockStepFunctionsClient = Substitute.ForPartsOf(); 119 | var mockIncidentRepository = Substitute.For(); 120 | 121 | var request = new APIGatewayProxyRequest 122 | { 123 | Body = JsonConvert.SerializeObject( 124 | new Dictionary 125 | { 126 | { "ExamId", "dc-not-a-guid" }, 127 | { "IncidentId", "6b44fd97-1af3-42f6-9a0b-0138fffa8cf4" }, 128 | { "Score", "65" }, 129 | { "TaskToken", Token } 130 | }) 131 | }; 132 | 133 | var context = new TestLambdaContext(); 134 | 135 | var expectedResponse = new APIGatewayProxyResponse 136 | { 137 | StatusCode = 400, 138 | Headers = new Dictionary 139 | { 140 | { "Content-Type", "application/json" }, 141 | { "Access-Control-Allow-Origin", "*" }, 142 | { "Access-Control-Allow-Headers", "Content-Type,X-Requested-With,Accept" }, 143 | { "Access-Control-Allow-Methods", "OPTIONS,POST" }, 144 | } 145 | }; 146 | 147 | var function = new Function(mockStepFunctionsClient, mockIncidentRepository); 148 | var response = function.FunctionHandler(request, context); 149 | 150 | _testOutputHelper.WriteLine("Lambda Response: \n" + response.StatusCode); 151 | _testOutputHelper.WriteLine("Expected Response: \n" + expectedResponse.StatusCode); 152 | 153 | Assert.Equal(expectedResponse.Headers, response.Headers); 154 | Assert.Equal(expectedResponse.StatusCode, response.StatusCode); 155 | } 156 | 157 | [Fact] 158 | public void SubmitExamHandlerReturns400ForInvalidIncidentIdRequest() 159 | { 160 | var mockStepFunctionsClient = Substitute.ForPartsOf(); 161 | var mockIncidentRepository = Substitute.For(); 162 | 163 | var request = new APIGatewayProxyRequest 164 | { 165 | Body = JsonConvert.SerializeObject(new Dictionary 166 | { 167 | { "ExamId", "dc149d4b-ce6d-435a-b922-9da90f7c3eed" }, 168 | { "IncidentId", "not-a-guid-0138fffa8cf4" }, 169 | { "Score", "65" }, 170 | { "TaskToken", Token } 171 | }) 172 | }; 173 | 174 | var context = new TestLambdaContext(); 175 | 176 | var expectedResponse = new APIGatewayProxyResponse 177 | { 178 | StatusCode = 400, 179 | Headers = new Dictionary 180 | { 181 | { "Content-Type", "application/json" }, 182 | { "Access-Control-Allow-Origin", "*" }, 183 | { "Access-Control-Allow-Headers", "Content-Type,X-Requested-With,Accept" }, 184 | { "Access-Control-Allow-Methods", "OPTIONS,POST" }, 185 | } 186 | }; 187 | 188 | var function = new Function(mockStepFunctionsClient, mockIncidentRepository); 189 | var response = function.FunctionHandler(request, context); 190 | 191 | _testOutputHelper.WriteLine("Lambda Response: \n" + response.StatusCode); 192 | _testOutputHelper.WriteLine("Expected Response: \n" + expectedResponse.StatusCode); 193 | 194 | Assert.Equal(expectedResponse.Headers, response.Headers); 195 | Assert.Equal(expectedResponse.StatusCode, response.StatusCode); 196 | } 197 | } -------------------------------------------------------------------------------- /src/backend/SubmitExam.Tests/SubmitExam.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/backend/SubmitExam.Tests/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "", 3 | "resource": "/{proxy+}", 4 | "path": "/path/to/resource", 5 | "httpMethod": "POST", 6 | "isBase64Encoded": false, 7 | "queryStringParameters": { 8 | "foo": "bar" 9 | }, 10 | "multiValueQueryStringParameters": { 11 | "foo": [ 12 | "bar" 13 | ] 14 | }, 15 | "pathParameters": { 16 | "proxy": "/path/to/resource" 17 | }, 18 | "stageVariables": { 19 | "baz": "qux" 20 | }, 21 | "headers": { 22 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 23 | "Accept-Encoding": "gzip, deflate, sdch", 24 | "Accept-Language": "en-US,en;q=0.8", 25 | "Cache-Control": "max-age=0", 26 | "CloudFront-Forwarded-Proto": "https", 27 | "CloudFront-Is-Desktop-Viewer": "true", 28 | "CloudFront-Is-Mobile-Viewer": "false", 29 | "CloudFront-Is-SmartTV-Viewer": "false", 30 | "CloudFront-Is-Tablet-Viewer": "false", 31 | "CloudFront-Viewer-Country": "US", 32 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 33 | "Upgrade-Insecure-Requests": "1", 34 | "User-Agent": "Custom User Agent String", 35 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 36 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 37 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 38 | "X-Forwarded-Port": "443", 39 | "X-Forwarded-Proto": "https" 40 | }, 41 | "multiValueHeaders": { 42 | "Accept": [ 43 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" 44 | ], 45 | "Accept-Encoding": [ 46 | "gzip, deflate, sdch" 47 | ], 48 | "Accept-Language": [ 49 | "en-US,en;q=0.8" 50 | ], 51 | "Cache-Control": [ 52 | "max-age=0" 53 | ], 54 | "CloudFront-Forwarded-Proto": [ 55 | "https" 56 | ], 57 | "CloudFront-Is-Desktop-Viewer": [ 58 | "true" 59 | ], 60 | "CloudFront-Is-Mobile-Viewer": [ 61 | "false" 62 | ], 63 | "CloudFront-Is-SmartTV-Viewer": [ 64 | "false" 65 | ], 66 | "CloudFront-Is-Tablet-Viewer": [ 67 | "false" 68 | ], 69 | "CloudFront-Viewer-Country": [ 70 | "US" 71 | ], 72 | "Host": [ 73 | "0123456789.execute-api.us-east-1.amazonaws.com" 74 | ], 75 | "Upgrade-Insecure-Requests": [ 76 | "1" 77 | ], 78 | "User-Agent": [ 79 | "Custom User Agent String" 80 | ], 81 | "Via": [ 82 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" 83 | ], 84 | "X-Amz-Cf-Id": [ 85 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" 86 | ], 87 | "X-Forwarded-For": [ 88 | "127.0.0.1, 127.0.0.2" 89 | ], 90 | "X-Forwarded-Port": [ 91 | "443" 92 | ], 93 | "X-Forwarded-Proto": [ 94 | "https" 95 | ] 96 | }, 97 | "requestContext": { 98 | "accountId": "123456789012", 99 | "resourceId": "123456", 100 | "stage": "prod", 101 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 102 | "requestTime": "09/Apr/2015:12:34:56 +0000", 103 | "requestTimeEpoch": 1428582896000, 104 | "identity": { 105 | "cognitoIdentityPoolId": null, 106 | "accountId": null, 107 | "cognitoIdentityId": null, 108 | "caller": null, 109 | "accessKey": null, 110 | "sourceIp": "127.0.0.1", 111 | "cognitoAuthenticationType": null, 112 | "cognitoAuthenticationProvider": null, 113 | "userArn": null, 114 | "userAgent": "Custom User Agent String", 115 | "user": null 116 | }, 117 | "path": "/prod/path/to/resource", 118 | "resourcePath": "/{proxy+}", 119 | "httpMethod": "POST", 120 | "apiId": "1234567890", 121 | "protocol": "HTTP/1.1" 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/backend/SubmitExam/Function.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Net; 7 | using Amazon.Lambda.APIGatewayEvents; 8 | using Amazon.Lambda.Core; 9 | using Amazon.StepFunctions; 10 | using Amazon.StepFunctions.Model; 11 | using AWS.Lambda.Powertools.Logging; 12 | using AWS.Lambda.Powertools.Metrics; 13 | using AWS.Lambda.Powertools.Tracing; 14 | using Newtonsoft.Json; 15 | using PlagiarismRepository; 16 | 17 | // Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. 18 | [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] 19 | 20 | namespace SubmitExam; 21 | 22 | public class Function 23 | { 24 | private readonly IIncidentRepository _incidentRepository; 25 | private readonly AmazonStepFunctionsClient _amazonStepFunctionsClient; 26 | 27 | /// 28 | /// Default constructor 29 | /// 30 | public Function() 31 | { 32 | Tracing.RegisterForAllServices(); 33 | _incidentRepository = new IncidentRepository(Environment.GetEnvironmentVariable("TABLE_NAME")); 34 | _amazonStepFunctionsClient = new AmazonStepFunctionsClient(); 35 | } 36 | 37 | /// 38 | /// Constructor used for testing purposes 39 | /// 40 | /// 41 | /// 42 | public Function(IAmazonStepFunctions stepFunctions, IIncidentRepository incidentRepository) 43 | { 44 | Tracing.RegisterForAllServices(); 45 | _incidentRepository = incidentRepository; 46 | _amazonStepFunctionsClient = (AmazonStepFunctionsClient)stepFunctions; 47 | } 48 | 49 | /// 50 | /// A simple function that takes a string and does a ToUpper 51 | /// 52 | /// Instance of APIGatewayProxyRequest 53 | /// AWS Lambda Context 54 | /// Instance of APIGatewayProxyResponse 55 | [Logging(LogEvent = true)] 56 | [Tracing(CaptureMode = TracingCaptureMode.ResponseAndError)] 57 | [Metrics(CaptureColdStart = true)] 58 | public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest request, ILambdaContext context) 59 | { 60 | var body = JsonConvert.DeserializeObject>(request?.Body); 61 | 62 | var isIncidentId = Guid.TryParse(body["IncidentId"], out var incidentId); 63 | var isExamId = Guid.TryParse(body["ExamId"], out var examId); 64 | var isScore = int.TryParse(body["Score"], out var score); 65 | 66 | var token = body["TaskToken"]; 67 | 68 | if (!isIncidentId || !isExamId | !isScore | !(token.Length >= 1 & token.Length <= 1024)) 69 | { 70 | Logger.LogInformation($"Invalid request: {request?.Body}\n\nIncidentId {incidentId} ExamId {examId} Score {score} Token {token}"); 71 | 72 | return ApiGatewayResponse(HttpStatusCode.BadRequest); 73 | 74 | } 75 | 76 | Logger.LogInformation("IncidentId: {incidentId}, ExamId: {examId}, Score: {score}, Token: {token}", 77 | incidentId, examId, score, token); 78 | 79 | var incident = _incidentRepository.GetIncidentById(incidentId); 80 | var exam = incident.Exams.Find(e => e.ExamId == examId); 81 | exam.Score = score; 82 | 83 | _incidentRepository.SaveIncident(incident); 84 | 85 | Logger.LogInformation(JsonConvert.SerializeObject(incident)); 86 | 87 | var sendTaskSuccessRequest = new SendTaskSuccessRequest 88 | { 89 | TaskToken = token, 90 | Output = JsonConvert.SerializeObject(incident) 91 | }; 92 | 93 | try 94 | { 95 | _amazonStepFunctionsClient.SendTaskSuccessAsync(sendTaskSuccessRequest).Wait(); 96 | } 97 | catch (Exception e) 98 | { 99 | Logger.LogError(e); 100 | return ApiGatewayResponse(HttpStatusCode.InternalServerError); 101 | } 102 | 103 | return ApiGatewayResponse(HttpStatusCode.OK); 104 | } 105 | 106 | /// 107 | /// Returns ApiGatewayResponse with specified status code 108 | /// 109 | /// HttpStatusCode 110 | /// Instance of ApiGatewayResponse 111 | private APIGatewayProxyResponse ApiGatewayResponse(HttpStatusCode statusCode) 112 | { 113 | 114 | return new APIGatewayProxyResponse 115 | { 116 | StatusCode = (int)statusCode, 117 | Headers = new Dictionary 118 | { 119 | { "Content-Type", "application/json" }, 120 | { "Access-Control-Allow-Origin", "*" }, 121 | { "Access-Control-Allow-Headers", "Content-Type,X-Requested-With,Accept" }, 122 | { "Access-Control-Allow-Methods", "OPTIONS,POST" } 123 | } 124 | }; 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /src/backend/SubmitExam/SubmitExam.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | true 5 | Lambda 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/backend/api.yaml: -------------------------------------------------------------------------------- 1 | 2 | openapi: "3.0.1" 3 | info: 4 | title: "PlagiarismApi" 5 | version: "1.0" 6 | description: "Plagiarism API allows students to submit their exams and lecturers to report plagiarism incidents." 7 | paths: 8 | /exam: 9 | post: 10 | responses: 11 | "200": 12 | description: "200 response" 13 | headers: 14 | Access-Control-Allow-Origin: 15 | schema: 16 | type: "string" 17 | content: 18 | application/json: 19 | schema: 20 | $ref: "#/components/schemas/Empty" 21 | x-amazon-apigateway-integration: 22 | type: "aws_proxy" 23 | httpMethod: "POST" 24 | uri: 25 | "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SubmitExamResultsFunction.Arn}/invocations" 26 | responses: 27 | default: 28 | statusCode: "200" 29 | responseParameters: 30 | method.response.header.Access-Control-Allow-Origin: "'*'" 31 | passthroughBehavior: "when_no_match" 32 | contentHandling: "CONVERT_TO_TEXT" 33 | options: 34 | responses: 35 | "200": 36 | description: "200 response" 37 | headers: 38 | Access-Control-Allow-Origin: 39 | schema: 40 | type: "string" 41 | Access-Control-Allow-Methods: 42 | schema: 43 | type: "string" 44 | Access-Control-Allow-Headers: 45 | schema: 46 | type: "string" 47 | content: 48 | application/json: 49 | schema: 50 | $ref: "#/components/schemas/Empty" 51 | x-amazon-apigateway-integration: 52 | type: "mock" 53 | responses: 54 | default: 55 | statusCode: "200" 56 | responseParameters: 57 | method.response.header.Access-Control-Allow-Methods: "'OPTIONS,POST'" 58 | method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Requested-With,Accept,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" 59 | method.response.header.Access-Control-Allow-Origin: "'*'" 60 | requestTemplates: 61 | application/json: "{\"statusCode\": 200}" 62 | passthroughBehavior: "when_no_match" 63 | /incident: 64 | post: 65 | responses: 66 | "200": 67 | description: "200 response" 68 | headers: 69 | Access-Control-Allow-Origin: 70 | schema: 71 | type: "string" 72 | content: 73 | application/json: 74 | schema: 75 | $ref: "#/components/schemas/Empty" 76 | x-amazon-apigateway-integration: 77 | type: "aws" 78 | credentials: 79 | "Fn::GetAtt": [ ApiGatewayStepFunctionsRole, Arn ] 80 | httpMethod: "POST" 81 | uri: 82 | "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:states:action/StartExecution" 83 | responses: 84 | default: 85 | statusCode: "200" 86 | responseParameters: 87 | method.response.header.Access-Control-Allow-Origin: "'*'" 88 | requestTemplates: 89 | application/json: 90 | "Fn::Sub": "{\"input\": \"$util.escapeJavaScript($input.json('$'))\",\"name\": \"$context.requestId\",\"stateMachineArn\": \"${PlagiarismStateMachine}\"}" 91 | passthroughBehavior: "when_no_templates" 92 | options: 93 | responses: 94 | "200": 95 | description: "200 response" 96 | headers: 97 | Access-Control-Allow-Origin: 98 | schema: 99 | type: "string" 100 | Access-Control-Allow-Methods: 101 | schema: 102 | type: "string" 103 | Access-Control-Allow-Headers: 104 | schema: 105 | type: "string" 106 | content: 107 | application/json: 108 | schema: 109 | $ref: "#/components/schemas/Empty" 110 | x-amazon-apigateway-integration: 111 | type: "mock" 112 | responses: 113 | default: 114 | statusCode: "200" 115 | responseParameters: 116 | method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'" 117 | method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Requested-With,Accept,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" 118 | method.response.header.Access-Control-Allow-Origin: "'*'" 119 | requestTemplates: 120 | application/json: "{\"statusCode\": 200}" 121 | passthroughBehavior: "when_no_match" 122 | components: 123 | schemas: 124 | Empty: 125 | title: "Empty Schema" 126 | type: "object" 127 | -------------------------------------------------------------------------------- /src/backend/omnisharp.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileOptions": { 3 | "excludeSearchPatterns": [ 4 | "**/bin/**/*", 5 | "**/obj/**/*" 6 | ] 7 | }, 8 | "msbuild": { 9 | "Platform": "rhel.7.2-x64" 10 | } 11 | } -------------------------------------------------------------------------------- /src/backend/state-machine.asl.yaml: -------------------------------------------------------------------------------- 1 | StartAt: Create new incident 2 | Comment: >- 3 | Plagiarism state machine manages the process for student's plagiarism 4 | violation. 5 | States: 6 | Create new incident: 7 | Type: Pass 8 | Next: Save incident 9 | Parameters: 10 | StudentId.$: $.StudentId 11 | IncidentDate.$: $.IncidentDate 12 | IncidentId.$: States.UUID() 13 | IncidentResolved: false 14 | AdminActionRequired: false 15 | 16 | Save incident: 17 | Type: Task 18 | Resource: arn:aws:states:::dynamodb:putItem 19 | Parameters: 20 | TableName: ${IncidentsTable} 21 | Item: 22 | StudentId: 23 | S.$: $.StudentId 24 | IncidentDate: 25 | S.$: $.IncidentDate 26 | IncidentId: 27 | S.$: $.IncidentId 28 | IncidentResolved: 29 | BOOL.$: $.IncidentResolved 30 | AdminActionRequired: 31 | BOOL.$: $.AdminActionRequired 32 | Next: Schedule exam 33 | ResultPath: null 34 | 35 | Schedule exam: 36 | Type: Task 37 | Comment: Set the next exam deadline for the student to complete the exam. 38 | Resource: arn:aws:states:::lambda:invoke 39 | Parameters: 40 | Payload.$: $ 41 | FunctionName: ${ScheduleExamFunctionArn} 42 | Retry: 43 | - ErrorEquals: 44 | - Lambda.ServiceException 45 | - Lambda.AWSLambdaException 46 | - Lambda.SdkClientException 47 | - Lambda.TooManyRequestsException 48 | IntervalSeconds: 1 49 | MaxAttempts: 3 50 | BackoffRate: 2 51 | Next: Notify student 52 | Catch: 53 | - ErrorEquals: 54 | - StudentExceededAllowableExamRetries 55 | ResultPath: $.Error 56 | Next: Take administrative action 57 | 58 | Notify student: 59 | Type: Task 60 | Resource: "arn:aws:states:::sns:publish.waitForTaskToken" 61 | Parameters: 62 | TopicArn: '${NotificationTopic}' 63 | Message.$: "States.Format('http://localhost:3000/?IncidentId={}&ExamId={}&TaskToken={}', $.Payload.IncidentId, $.Payload.Exams[0].ExamId, $$.Task.Token)" 64 | Next: Has student passed exam? 65 | 66 | Has student passed exam?: 67 | Type: Choice 68 | Comment: If the student has a score less than 67, they need to reschedule 69 | Choices: 70 | - Variable: $.Exams[0].Result 71 | NumericEquals: 0 72 | Next: Resolve incident 73 | - Variable: $.Exams[0].Result 74 | NumericEquals: 1 75 | Next: Schedule exam 76 | - Variable: $.Exams[0].Result 77 | NumericEquals: 2 78 | Next: Take administrative action 79 | 80 | Take administrative action: 81 | Type: Task 82 | Comment: >- 83 | Take administrative action if student does not sit exam or fails all three 84 | attempts. 85 | Resource: arn:aws:states:::lambda:invoke 86 | Parameters: 87 | Payload.$: $ 88 | FunctionName: ${TakeAdministrativeActionFunctionArn} 89 | Retry: 90 | - ErrorEquals: 91 | - Lambda.ServiceException 92 | - Lambda.AWSLambdaException 93 | - Lambda.SdkClientException 94 | - Lambda.TooManyRequestsException 95 | IntervalSeconds: 1 96 | MaxAttempts: 3 97 | BackoffRate: 2 98 | End: true 99 | 100 | Resolve incident: 101 | Type: Task 102 | Comment: Resolves the incident for the student. 103 | Resource: arn:aws:states:::lambda:invoke 104 | Parameters: 105 | Payload.$: $ 106 | FunctionName: ${ResolveIncidentFunctionArn} 107 | Retry: 108 | - ErrorEquals: 109 | - Lambda.ServiceException 110 | - Lambda.AWSLambdaException 111 | - Lambda.SdkClientException 112 | - Lambda.TooManyRequestsException 113 | IntervalSeconds: 1 114 | MaxAttempts: 3 115 | BackoffRate: 2 116 | End: true 117 | -------------------------------------------------------------------------------- /src/backend/template.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | AWSTemplateFormatVersion: "2010-09-09" 4 | Transform: 5 | - AWS::LanguageExtensions 6 | - AWS::Serverless-2016-10-31 7 | Description: > 8 | Developing with Step Functions Demo application. Scenario - University students caught plagiarising on exams 9 | and assignments are asked required to take exams to test that know how to reference properly. 10 | Students get three attempts before action is taken. This demo uses exposes an AWS Step Function via an Amazon API Gateway. 11 | The step-function definition invokes tasks via AWS Lambda (.NET 8), that store results in Amazon DynamoDB. 12 | Notifications are implemented via Amazon SNS and AWS X-Ray provides distributed tracing capability. 13 | 14 | Metadata: 15 | cfn-lint: 16 | config: 17 | ignore_checks: 18 | - ES4000 # Rule disabled because the CatchAll Rule doesn't need a DLQ 19 | - ES6000 # Rule disabled because SQS DLQs don't need a RedrivePolicy 20 | - WS2001 # Rule disabled because check does not support !ToJsonString transform 21 | - ES1001 # Rule disabled because our Lambda functions don't need DestinationConfig.OnFailure 22 | - ES7000 # Rule disabled because SNS doesn't need a DLQ 23 | 24 | Parameters: 25 | Stage: 26 | Type: String 27 | Default: dev 28 | AllowedValues: 29 | - dev 30 | - prod 31 | 32 | ToEmail: 33 | Type: String 34 | Description: Student email (testing only) 35 | 36 | 37 | Conditions: 38 | IsProd: !Equals [!Ref Stage, Prod] 39 | 40 | Mappings: 41 | LogsRetentionPeriodMap: 42 | local: 43 | Days: 3 44 | dev: 45 | Days: 3 46 | prod: 47 | Days: 14 48 | 49 | # Globals 50 | Globals: 51 | 52 | Function: 53 | Runtime: dotnet8 54 | Timeout: 15 55 | MemorySize: 512 56 | Architectures: 57 | - x86_64 58 | Tracing: Active 59 | Environment: 60 | Variables: 61 | TABLE_NAME: !Ref PlagiarismIncidentsTable 62 | SERVICE_NAMESPACE: "Plagiarism" 63 | POWERTOOLS_LOGGER_CASE: "PascalCase" 64 | POWERTOOLS_SERVICE_NAME: "Plagiarism" 65 | POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default 66 | POWERTOOLS_LOGGER_LOG_EVENT: "true" # Logs incoming event, default 67 | POWERTOOLS_LOGGER_SAMPLE_RATE: "0" # Debug log sampling percentage, default 68 | POWERTOOLS_METRICS_NAMESPACE: "Plagiarism" 69 | POWERTOOLS_LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default 70 | LOG_LEVEL: INFO # Log level for Logger 71 | 72 | Resources: 73 | #### Lambda functions 74 | 75 | ScheduleExamFunction: 76 | Type: AWS::Serverless::Function 77 | Properties: 78 | Description: Plagiarism - Schedules the Exam for the student to complete. 79 | CodeUri: ./ScheduleExam 80 | Handler: ScheduleExam::ScheduleExam.Function::FunctionHandler 81 | Policies: 82 | - DynamoDBWritePolicy: 83 | TableName: !Ref PlagiarismIncidentsTable 84 | - DynamoDBReadPolicy: 85 | TableName: !Ref PlagiarismIncidentsTable 86 | 87 | 88 | # User defined log group for the ScheduleExamFunction 89 | ScheduleExamFunctionLogGroup: 90 | Type: AWS::Logs::LogGroup 91 | DeletionPolicy: Delete 92 | UpdateReplacePolicy: Delete 93 | Properties: 94 | LogGroupName: !Sub "/aws/lambda/${ScheduleExamFunction}" 95 | RetentionInDays: !FindInMap 96 | - LogsRetentionPeriodMap 97 | - !Ref Stage 98 | - Days 99 | 100 | ResolveIncidentFunction: 101 | Type: AWS::Serverless::Function 102 | Properties: 103 | Description: Plagiarism - Marks the incident as resolved. 104 | CodeUri: ./ResolveIncident 105 | Handler: ResolveIncident::ResolveIncident.Function::FunctionHandler 106 | Policies: 107 | - DynamoDBWritePolicy: 108 | TableName: !Ref PlagiarismIncidentsTable 109 | - DynamoDBReadPolicy: 110 | TableName: !Ref PlagiarismIncidentsTable 111 | 112 | # User defined log group for the ResolveIncidentFunction 113 | ResolveIncidentFunctionLogGroup: 114 | Type: AWS::Logs::LogGroup 115 | DeletionPolicy: Delete 116 | UpdateReplacePolicy: Delete 117 | Properties: 118 | LogGroupName: !Sub "/aws/lambda/${ResolveIncidentFunction}" 119 | RetentionInDays: !FindInMap 120 | - LogsRetentionPeriodMap 121 | - !Ref Stage 122 | - Days 123 | 124 | TakeAdministrativeActionFunction: 125 | Type: AWS::Serverless::Function 126 | Properties: 127 | Description: Plagiarism - Send email to administrative staff to notify staff that the student has failed all tests and action needs to be taken. 128 | CodeUri: ./AdminAction/ 129 | Handler: AdminAction::AdminAction.Function::FunctionHandler 130 | Policies: 131 | - DynamoDBWritePolicy: 132 | TableName: !Ref PlagiarismIncidentsTable 133 | - DynamoDBReadPolicy: 134 | TableName: !Ref PlagiarismIncidentsTable 135 | 136 | # User defined log group for the TakeAdministrativeActionFunction 137 | TakeAdministrativeActionFunctionLogGroup: 138 | Type: AWS::Logs::LogGroup 139 | DeletionPolicy: Delete 140 | UpdateReplacePolicy: Delete 141 | Properties: 142 | LogGroupName: !Sub "/aws/lambda/${TakeAdministrativeActionFunction}" 143 | RetentionInDays: !FindInMap 144 | - LogsRetentionPeriodMap 145 | - !Ref Stage 146 | - Days 147 | 148 | SubmitExamResultsFunction: 149 | Type: AWS::Serverless::Function 150 | Properties: 151 | Description: Plagiarism - Saves the test results and invokes the callback to the SendExamNotification state in the Step Function 152 | CodeUri: ./SubmitExam 153 | Handler: SubmitExam::SubmitExam.Function::FunctionHandler 154 | Policies: 155 | - DynamoDBWritePolicy: 156 | TableName: !Ref PlagiarismIncidentsTable 157 | - DynamoDBReadPolicy: 158 | TableName: !Ref PlagiarismIncidentsTable 159 | - Statement: 160 | - Effect: Allow 161 | Action: 162 | - states:SendTaskSuccess 163 | - states:SendTaskFailure 164 | Resource: !Ref PlagiarismStateMachine 165 | Events: 166 | StepApi: 167 | Type: Api 168 | Properties: 169 | Path: /exam 170 | Method: post 171 | RestApiId: !Ref PlagiarismApi 172 | 173 | SubmitExamResultsFunctionLogGroup: 174 | Type: AWS::Logs::LogGroup 175 | DeletionPolicy: Delete 176 | UpdateReplacePolicy: Delete 177 | Properties: 178 | LogGroupName: !Sub "/aws/lambda/${SubmitExamResultsFunction}" 179 | RetentionInDays: !FindInMap 180 | - LogsRetentionPeriodMap 181 | - !Ref Stage 182 | - Days 183 | 184 | # API Gateway 185 | PlagiarismApi: 186 | Type: AWS::Serverless::Api 187 | DependsOn: PlagiarismApiGwAccountConfig 188 | Properties: 189 | Name: !Sub "Plagiarism-API-${Stage}" 190 | StageName: !Ref Stage 191 | EndpointConfiguration: 192 | Type: REGIONAL 193 | TracingEnabled: true 194 | # More info about OpenApiVersion: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html#sam-api-openapiversion 195 | OpenApiVersion: 3.0.1 196 | MethodSettings: 197 | - MetricsEnabled: true 198 | ResourcePath: /* 199 | HttpMethod: "*" 200 | LoggingLevel: !If [ IsProd, ERROR, INFO ] 201 | ThrottlingBurstLimit: 10 202 | ThrottlingRateLimit: 100 203 | AccessLogSetting: 204 | DestinationArn: !GetAtt PlagiarismApiLogGroup.Arn 205 | Format: !ToJsonString 206 | requestId: $context.requestId 207 | integration-error: $context.integration.error 208 | integration-status: $context.integration.status 209 | integration-latency: $context.integration.latency 210 | integration-requestId: $context.integration.requestId 211 | integration-integrationStatus: $context.integration.integrationStatus 212 | response-latency: $context.responseLatency 213 | status: $context.status 214 | DefinitionBody: !Transform 215 | Name: "AWS::Include" 216 | Parameters: 217 | Location: "./api.yaml" 218 | 219 | # API GW Cloudwatch Log Group 220 | PlagiarismApiLogGroup: 221 | Type: AWS::Logs::LogGroup 222 | UpdateReplacePolicy: Delete 223 | DeletionPolicy: Delete 224 | Properties: 225 | RetentionInDays: !FindInMap [ LogsRetentionPeriodMap, !Ref Stage, Days ] 226 | 227 | # API Gateway Account Configuration, to enable Logs to be sent to CloudWatch 228 | PlagiarismApiGwAccountConfig: 229 | Type: AWS::ApiGateway::Account 230 | Properties: 231 | CloudWatchRoleArn: !GetAtt PlagiarismApiGwAccountConfigRole.Arn 232 | 233 | # API GW IAM roles 234 | PlagiarismApiGwAccountConfigRole: 235 | Type: AWS::IAM::Role 236 | Properties: 237 | AssumeRolePolicyDocument: 238 | Statement: 239 | - Effect: Allow 240 | Action: sts:AssumeRole 241 | Principal: 242 | Service: apigateway.amazonaws.com 243 | ManagedPolicyArns: 244 | - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs 245 | 246 | # Step Function 247 | # More info about State Machine Resource: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-statemachine.html 248 | PlagiarismStateMachine: 249 | Type: "AWS::Serverless::StateMachine" 250 | Properties: 251 | Name: !Sub "PlagiarismStateMachine-${Stage}" 252 | DefinitionUri: state-machine.asl.yaml 253 | Tracing: 254 | Enabled: true 255 | DefinitionSubstitutions: 256 | ScheduleExamFunctionArn: !GetAtt ScheduleExamFunction.Arn 257 | TakeAdministrativeActionFunctionArn: !GetAtt TakeAdministrativeActionFunction.Arn 258 | ResolveIncidentFunctionArn: !GetAtt ResolveIncidentFunction.Arn 259 | NotificationTopic: !Ref PlagiarismTopic 260 | IncidentsTable: !Ref PlagiarismIncidentsTable 261 | Policies: 262 | - LambdaInvokePolicy: 263 | FunctionName: !Ref ScheduleExamFunction 264 | - LambdaInvokePolicy: 265 | FunctionName: !Ref TakeAdministrativeActionFunction 266 | - LambdaInvokePolicy: 267 | FunctionName: !Ref ResolveIncidentFunction 268 | - DynamoDBWritePolicy: 269 | TableName: !Ref PlagiarismIncidentsTable 270 | - DynamoDBReadPolicy: 271 | TableName: !Ref PlagiarismIncidentsTable 272 | - SNSPublishMessagePolicy: 273 | TopicName: !GetAtt PlagiarismTopic.TopicName 274 | - Statement: 275 | - Effect: Allow 276 | Action: 277 | - logs:CreateLogDelivery 278 | - logs:GetLogDelivery 279 | - logs:UpdateLogDelivery 280 | - logs:DeleteLogDelivery 281 | - logs:ListLogDeliveries 282 | - logs:PutResourcePolicy 283 | - logs:DescribeResourcePolicies 284 | - logs:DescribeLogGroups 285 | - cloudwatch:PutMetricData 286 | Resource: "*" 287 | Logging: 288 | Destinations: 289 | - CloudWatchLogsLogGroup: 290 | LogGroupArn: !GetAtt PlagiarismStateMachineLogGroup.Arn 291 | Level: ALL 292 | IncludeExecutionData: true 293 | 294 | # Store PlagiarismStateMachineLogGroup workflow execution logs 295 | PlagiarismStateMachineLogGroup: 296 | Type: AWS::Logs::LogGroup 297 | UpdateReplacePolicy: Delete 298 | DeletionPolicy: Delete 299 | Properties: 300 | LogGroupName: !Sub "/aws/states/PlagiarismStateMachine-${Stage}" 301 | RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days] 302 | 303 | # SNS Topic to use in Step Functions 304 | PlagiarismTopic: 305 | Type: AWS::SNS::Topic 306 | 307 | # Email subscription for topic 308 | PlagiarismSubscription: 309 | Type: AWS::SNS::Subscription 310 | Properties: 311 | Endpoint: !Ref ToEmail 312 | Protocol: email 313 | TopicArn: !Ref PlagiarismTopic 314 | 315 | # DynamoDB 316 | PlagiarismIncidentsTable: 317 | Type: AWS::Serverless::SimpleTable 318 | UpdateReplacePolicy: Delete 319 | DeletionPolicy: Delete 320 | Properties: 321 | TableName: !Sub "PlagiarismIncidents-${Stage}" 322 | PrimaryKey: 323 | Name: IncidentId 324 | Type: String 325 | ProvisionedThroughput: 326 | ReadCapacityUnits: 5 327 | WriteCapacityUnits: 5 328 | 329 | 330 | # IAM roles 331 | ApiGatewayStepFunctionsRole: 332 | Type: "AWS::IAM::Role" 333 | Properties: 334 | AssumeRolePolicyDocument: 335 | Version: "2012-10-17" 336 | Statement: 337 | - Sid: "AllowApiGatewayServiceToAssumeRole" 338 | Effect: "Allow" 339 | Action: 340 | - "sts:AssumeRole" 341 | Principal: 342 | Service: 343 | - "apigateway.amazonaws.com" 344 | Policies: 345 | - PolicyName: "CallStepFunctions" 346 | PolicyDocument: 347 | Version: '2012-10-17' 348 | Statement: 349 | - Effect: "Allow" 350 | Action: 351 | - "states:StartExecution" 352 | Resource: 353 | - !Ref PlagiarismStateMachine 354 | 355 | LambdaExecutionRole: 356 | Type: "AWS::IAM::Role" 357 | Properties: 358 | ManagedPolicyArns: 359 | - "arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess" 360 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 361 | AssumeRolePolicyDocument: 362 | Version: "2012-10-17" 363 | Statement: 364 | - Sid: "AllowLambdaServiceToAssumeRole" 365 | Effect: "Allow" 366 | Action: 367 | - "sts:AssumeRole" 368 | Principal: 369 | Service: 370 | - "lambda.amazonaws.com" 371 | 372 | IncidentsTableAccessRole: 373 | Type: "AWS::IAM::Role" 374 | Properties: 375 | ManagedPolicyArns: 376 | - "arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess" 377 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 378 | AssumeRolePolicyDocument: 379 | Version: "2012-10-17" 380 | Statement: 381 | - Sid: "AllowLambdaServiceToAssumeRole" 382 | Effect: "Allow" 383 | Action: 384 | - "sts:AssumeRole" 385 | Principal: 386 | Service: 387 | - "lambda.amazonaws.com" 388 | Policies: 389 | - PolicyName: "AllowCRUDOperationsOnDynamoDB" 390 | PolicyDocument: 391 | Version: '2012-10-17' 392 | Statement: 393 | - Effect: "Allow" 394 | Action: 395 | - 'dynamodb:GetItem' 396 | - 'dynamodb:DeleteItem' 397 | - 'dynamodb:DescribeTable' 398 | - 'dynamodb:PutItem' 399 | - 'dynamodb:Scan' 400 | - 'dynamodb:Query' 401 | - 'dynamodb:UpdateItem' 402 | - 'dynamodb:BatchWriteItem' 403 | - 'dynamodb:BatchGetItem' 404 | Resource: 405 | - !Sub ${PlagiarismIncidentsTable.Arn} 406 | - PolicyName: "AllowStatesActions" 407 | PolicyDocument: 408 | Version: '2012-10-17' 409 | Statement: 410 | - Effect: "Allow" 411 | Action: 412 | - 'states:SendTaskSuccess' 413 | - 'states:SendTaskFailure' 414 | - 'states:SendTaskHeartbeat' 415 | Resource: 416 | - "*" 417 | Outputs: 418 | 419 | ApiEndpointRegisterIncident: 420 | Description: API endpoint for registering an incident 421 | Value: !Sub "https://${PlagiarismApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/incident" 422 | 423 | ApiEndpointSubmitExamResults: 424 | Description: API endpoint for submitting exam results 425 | Value: !Sub "https://${PlagiarismApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/exam" 426 | 427 | StepFunctionsStateMachine: 428 | Description: Step Functions State Machine ARN 429 | Value: !Ref PlagiarismStateMachine 430 | 431 | ScheduleExamFunctionArn: 432 | Description: Schedule Exam Function ARN 433 | Value: !GetAtt ScheduleExamFunction.Arn 434 | 435 | SubmitExamResultsFunctionArn: 436 | Description: Submit Exam Function Function ARN 437 | Value: !GetAtt SubmitExamResultsFunction.Arn 438 | 439 | ResolveIncidentFunctionArn: 440 | Description: Resolve Incident Function ARN 441 | Value: !GetAtt ResolveIncidentFunction.Arn 442 | 443 | TakeAdministrativeActionFunctionArn: 444 | Description: Take Administrative Action Function ARN 445 | Value: !GetAtt TakeAdministrativeActionFunction.Arn 446 | -------------------------------------------------------------------------------- /src/frontend/.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_ENDPOINT="REPLACE API GATEWAY URL HERE" -------------------------------------------------------------------------------- /src/frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | #amplify-do-not-edit-begin 39 | amplify/\#current-cloud-backend 40 | amplify/.config/local-* 41 | amplify/logs 42 | amplify/mock-data 43 | amplify/mock-api-resources 44 | amplify/backend/amplify-meta.json 45 | amplify/backend/.temp 46 | build/ 47 | dist/ 48 | node_modules/ 49 | aws-exports.js 50 | awsconfiguration.json 51 | amplifyconfiguration.json 52 | amplifyconfiguration.dart 53 | amplify-build-config.json 54 | amplify-gradle-config.json 55 | amplifytools.xcconfig 56 | .secret-* 57 | **.sample 58 | #amplify-do-not-edit-end 59 | -------------------------------------------------------------------------------- /src/frontend/README.md: -------------------------------------------------------------------------------- 1 | # AWS Step Functions Plagiarism Demo - Test Centre 2 | 3 | An app designed to test students as part of an AWS Step Functions workflow. 4 | 5 | It can accept GET variables to refer to an exam attempt: IncidentId, ExamId, 6 | Test Centre. 7 | 8 | ![TestCentre Screenshot](testcentre-screenshot.png) 9 | 10 | ## Test Centre role in Plagiarism Step Functions workflow 11 | 12 | The flow for use of the Test Centre is: 13 | 14 | - A Step Functions execution is initiated. 15 | - An exam attempt is generated. 16 | - A mail is sent to the student, with a link in the following format: 17 | 18 | `GET https://testcentre.example.com/?ExamID=foo&IncidentId=bar&TaskToken=baz` 19 | 20 | - A student then completes a test. Once done, a request is sent back to Step Functions as a POST request, to an API Gateway endpoint, with their score and the task token: 21 | 22 | `POST https://1234567890-abcdefgh.amazonaws.com/submitExam` 23 | 24 | **Payload** 25 | ``` 26 | { 27 | "ExamId": "foo", 28 | "IncidentId": "bar", 29 | "TaskToken": "baz", 30 | "Score": "69" 31 | } 32 | ``` 33 | 34 | # Deploying 35 | 36 | 1. This project is designed to deploy via the Amplify Console. 37 | 38 | 2. It assumes you have already deployed the parent `AWSStepFunctionsPlagiarismDemo.` 39 | 40 | 3. Get the API Gateway endpoint after it is deployed: 41 | 42 | `aws cloudformation describe-stacks --stack-name aws-step-functions-plagiarism-demo --query 'Stacks[].Outputs' | grep api` 43 | 44 | 4. Then, create a new Amplify Console project. 45 | 46 | 5. Connect it to a repository hosting this code, then set an enviromnet variable called `NEXT_PUBLIC_API_ENDPOINT` in the .env file with the value that you just got from the CloudFormation output. 47 | 48 | 6. Deploy the site. 49 | 50 | ## Project setup 51 | ### Install dependencies 52 | 53 | ``` 54 | pnpm install 55 | ``` 56 | 57 | ### Compile and hot-reloads for development 58 | ``` 59 | pnpm dev 60 | ``` 61 | 62 | ### Compile and minify for production 63 | ``` 64 | pnpm build 65 | ``` 66 | ### Lint and fix files 67 | ``` 68 | pnpm run lint 69 | ``` 70 | 71 | -------------------------------------------------------------------------------- /src/frontend/amplify.yml: -------------------------------------------------------------------------------- 1 | # This Amplify Console build configuration will deploy the Test Centre website. 2 | # You will need to set an environment variable in Amplify, defining the API 3 | # Gateway endpoint for your Step Functions callback interface. 4 | # The environment variable should be called APIGW_ENDPOINT 5 | # See src/frontend/testcentre/README.md for more details. 6 | version: 0.1 7 | frontend: 8 | phases: 9 | build: 10 | commands: 11 | - cd src/frontend 12 | - pnpm install 13 | - pnpm run build 14 | - pnpm run deploy 15 | artifacts: 16 | baseDirectory: src/frontend/out 17 | files: 18 | - '**/*' 19 | cache: 20 | paths: [] 21 | -------------------------------------------------------------------------------- /src/frontend/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | images: { 6 | unoptimized: true, 7 | }, 8 | output: 'export' 9 | }; 10 | 11 | 12 | module.exports = nextConfig 13 | -------------------------------------------------------------------------------- /src/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "serve out", 9 | "lint": "next lint", 10 | "deploy": "amplify push" 11 | }, 12 | "dependencies": { 13 | "@aws-amplify/cli": "^12.12.6", 14 | "bootstrap": "^5.3.3", 15 | "bulma": "^0.9.4", 16 | "next": "14.2.13", 17 | "react": "^18.3.1", 18 | "react-dom": "^18.3.1", 19 | "sass": "^1.79.3", 20 | "serve": "^14.2.3" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^20.16.5", 24 | "@types/react": "^18.3.8", 25 | "@types/react-dom": "^18.3.0", 26 | "eslint": "^8.57.1", 27 | "eslint-config-next": "14.0.3", 28 | "typescript": "^5.6.2" 29 | }, 30 | "packageManager": "pnpm@9.1.1+sha512.14e915759c11f77eac07faba4d019c193ec8637229e62ec99eefb7cf3c3b75c64447882b7c485142451ee3a6b408059cdfb7b7fa0341b975f12d0f7629c71195" 31 | } 32 | -------------------------------------------------------------------------------- /src/frontend/public/images/AWS_logo_RGB_REV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-step-functions-plagiarism-demo-dotnet/5514aaa30ca5c5cc563f3fbf017de5117c1dae99/src/frontend/public/images/AWS_logo_RGB_REV.png -------------------------------------------------------------------------------- /src/frontend/public/images/aws-smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-step-functions-plagiarism-demo-dotnet/5514aaa30ca5c5cc563f3fbf017de5117c1dae99/src/frontend/public/images/aws-smile.png -------------------------------------------------------------------------------- /src/frontend/public/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-step-functions-plagiarism-demo-dotnet/5514aaa30ca5c5cc563f3fbf017de5117c1dae99/src/frontend/public/images/bg.png -------------------------------------------------------------------------------- /src/frontend/public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-step-functions-plagiarism-demo-dotnet/5514aaa30ca5c5cc563f3fbf017de5117c1dae99/src/frontend/public/images/favicon.ico -------------------------------------------------------------------------------- /src/frontend/public/images/header-icon_step-functions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-step-functions-plagiarism-demo-dotnet/5514aaa30ca5c5cc563f3fbf017de5117c1dae99/src/frontend/public/images/header-icon_step-functions.png -------------------------------------------------------------------------------- /src/frontend/public/images/stepfunction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-step-functions-plagiarism-demo-dotnet/5514aaa30ca5c5cc563f3fbf017de5117c1dae99/src/frontend/public/images/stepfunction.png -------------------------------------------------------------------------------- /src/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | AWS Step Functions Plagiarism Demo - Test Centre 8 | 9 | 10 | 13 |
14 |
15 |

AWS Step Functions Plagiarism Exam

16 |

17 | Please re-attempt the exam. Once you are done, submit your exam for marking. 18 |

19 |
20 |
21 | 22 |
23 |
24 |
25 |
26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/src/api/api.ts: -------------------------------------------------------------------------------- 1 | const API_ENDPOINT = process.env.NEXT_PUBLIC_API_ENDPOINT; 2 | 3 | export interface ExamData { 4 | // Exam score (out of 100). 5 | Score: number, 6 | // Unique identifier for plagiarism incident. 7 | IncidentId: string, 8 | // Unique identifier for exam attempt. 9 | ExamId: string, 10 | // Task Token unique to the current Step Functions execution. 11 | TaskToken: string 12 | } 13 | 14 | export async function submitExam(examData: ExamData) { 15 | console.log(process.env); 16 | console.log(API_ENDPOINT); 17 | const response = await fetch(`${API_ENDPOINT}/exam`, { method: 'POST', mode: 'cors', body: JSON.stringify(examData) }); 18 | if (!response.ok) throw (response); 19 | return await response.json(); 20 | } 21 | 22 | export interface Incident { 23 | StudentId: string, 24 | IncidentDate: string 25 | } 26 | 27 | export interface StepFunctionInfo { 28 | executionArn: string 29 | startDate: string 30 | } 31 | 32 | export async function createIncident(incidentData: Incident): Promise { 33 | const response = await fetch(`${API_ENDPOINT}/incident`, { method: 'POST', mode: 'cors', headers: { "Content-Type": "application/json" }, body: JSON.stringify(incidentData) }); 34 | const { executionArn, startDate } = await response.json(); 35 | return { executionArn, startDate }; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/frontend/src/app/admin/custom.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | 4 | // Colours. 5 | // Taken from AWS Brand Book (July 2018). 6 | // Primary brand colour 7 | $aws_orange: #FF9900; 8 | // Primary neutral brand colour. 9 | $aws_squidink: #232F3E; 10 | //$heading-color: rgb(35, 47, 62); // this is the same as squidink 11 | //$aws_bg_grey: #f7f7f7; 12 | // Secondary Data. 13 | $aws_mercury: #545b64; 14 | 15 | // Customizing Bulma variables. 16 | $dark: $aws_squidink; 17 | $primary: $aws_orange; 18 | $white: 'red'; 19 | 20 | // Images. 21 | 22 | body, :root { 23 | background-color: #2B3E50; 24 | } 25 | 26 | .jumbotron { 27 | padding: 2rem 1rem; 28 | margin-bottom: 2rem; 29 | background-color: #4E5D6C; 30 | border-radius: 0; 31 | } 32 | 33 | .bg-secondary { 34 | background-color: #4E5D6C !important; 35 | } 36 | 37 | .text-primary, a { 38 | color: #DF691A !important; 39 | } 40 | 41 | a { 42 | text-decoration: none; 43 | } 44 | 45 | a:hover { 46 | color: #9a4912; 47 | text-decoration: underline; 48 | } 49 | 50 | .form-group { 51 | margin-bottom: 1rem; 52 | } 53 | 54 | .form-text { 55 | display: block; 56 | margin-top: 0.25rem; 57 | } 58 | 59 | .form-row { 60 | display: -webkit-box; 61 | display: -ms-flexbox; 62 | display: flex; 63 | -ms-flex-wrap: wrap; 64 | flex-wrap: wrap; 65 | margin-right: -5px; 66 | margin-left: -5px; 67 | } 68 | 69 | .btn-primary { 70 | color: #fff; 71 | background-color: #DF691A; 72 | border-color: #DF691A; 73 | border: 1px solid transparent; 74 | padding: 0.375rem 0.75rem; 75 | } 76 | 77 | .btn-primary:hover { 78 | color: #fff; 79 | background-color: #bd5916; 80 | border-color: #b15315; 81 | } 82 | 83 | .card-header { 84 | padding: 0.75rem 1.25rem; 85 | margin-bottom: 0; 86 | background-color: rgba(255, 255, 255, 0.075); 87 | border-bottom: 1px solid rgba(0, 0, 0, 0.125); 88 | } -------------------------------------------------------------------------------- /src/frontend/src/app/admin/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { Inter } from 'next/font/google' 3 | import './custom.scss' 4 | 5 | 6 | export const metadata: Metadata = { 7 | title: 'Developing with Step Functions', 8 | description: '', 9 | } 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: { 14 | children: React.ReactNode 15 | }) { 16 | return ( 17 | 18 | 19 | {children} 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/frontend/src/app/admin/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { FormEvent, useRef, useState } from 'react' 4 | import { Incident, createIncident } from '@/api/api'; 5 | 6 | 7 | 8 | export default function AdminPage({ }) { 9 | const [showMessage, setShowMessage] = useState(false); 10 | const [executionArn, setExecutionArn] = useState(undefined); 11 | const [executionStartDate, setExecutionStartDate] = useState(undefined); 12 | const [submittedStudentId, setSubmittedStudentId] = useState(undefined); 13 | 14 | const studentIdInputRef = useRef(null); 15 | const incidentDateInputRef = useRef(null); 16 | 17 | 18 | function resetForm(e: FormEvent) { 19 | e.preventDefault(); 20 | if(studentIdInputRef?.current) studentIdInputRef.current.value = ''; 21 | if(incidentDateInputRef?.current) incidentDateInputRef.current.value = ''; 22 | setShowMessage(false); 23 | } 24 | 25 | async function incidentFormSubmitted(e: FormEvent) { 26 | e.preventDefault(); 27 | if(!studentIdInputRef?.current || !incidentDateInputRef?.current) return; 28 | const {executionArn: incidentExecutionArn, startDate: incidentStartDate} = await createIncident({StudentId: studentIdInputRef.current.value, IncidentDate: incidentDateInputRef.current.value}); 29 | setSubmittedStudentId(studentIdInputRef.current.value); 30 | setExecutionArn(incidentExecutionArn); 31 | setExecutionStartDate(incidentStartDate); 32 | setShowMessage(true); 33 | } 34 | 35 | 36 | return ( 37 |
38 |
39 |
40 |
41 | AWS 42 |
43 |

Developing with AWS AWS Step Functions

44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
incidentFormSubmitted(e)}> 53 |

Create new plagiarism incident

54 |
55 | 56 | 57 |
58 |
59 | 60 | 61 |
62 |
63 | 64 | 65 |
66 |
67 |
68 |
69 |
70 | {showMessage && ( 71 | <> 72 |
73 | Created new incident for Student ID: {submittedStudentId}
74 |
75 |
76 | Execution ARN: {executionArn}
77 | Start Date: {executionStartDate} 78 |
79 | 80 | )} 81 |
82 | 83 |
84 |
85 |
86 |
SCENARIO
87 |
88 |

University students caught plagiarising on exams and assignments are required to take exams 89 | to test their knowledge of the universities referencing standard. Students get three attempts to pass the exam 90 | before administrative action is taken.

91 | 92 |

This demo uses exposes an AWS Step Function via an Amazon API Gateway. The step-function definition invokes 93 | tasks via AWS Lambda, that store results in Amazon DynamoDB. Notifications are implemented 94 | via Amazon SNS and AWS X-Ray provides distributed tracing capability.

95 |
96 |
97 | state-machine 98 | 99 |
100 |
101 | 102 | 103 | 104 | 105 |
106 | ) 107 | } 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/frontend/src/app/globals.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | // Colours. 3 | // Taken from AWS Brand Book (July 2018). 4 | // Primary brand colour 5 | $aws_orange: #FF9900; 6 | // Primary neutral brand colour. 7 | $aws_squidink: #232F3E; 8 | //$heading-color: rgb(35, 47, 62); // this is the same as squidink 9 | //$aws_bg_grey: #f7f7f7; 10 | // Secondary Data. 11 | $aws_mercury: #545b64; 12 | $white: #FFF; 13 | 14 | // Customizing Bulma variables. 15 | $dark: $aws_squidink; 16 | 17 | // Images. 18 | // @todo investigate file-loader to avoid keeping images in /public. 19 | $asset_path: "/images"; 20 | $aws_logo_img: "#{$asset_path}/aws-smile.png"; 21 | $aws_hero_bg_img: "#{$asset_path}/bg.png"; 22 | 23 | // Main title 24 | $title_color: $aws_squidink; 25 | 26 | // Navbar 27 | $navbar-background-color: $aws_squidink; 28 | 29 | #exam-hero { 30 | background-image: 31 | url($aws_logo_img), 32 | url($aws_hero_bg_img); 33 | background-position: 34 | right 1em top 1em, 35 | right; 36 | background-repeat: 37 | no-repeat, 38 | no-repeat; 39 | margin-bottom: -1.5em; 40 | } 41 | 42 | .exam { 43 | margin-bottom: 1em; 44 | .question, 45 | .submit { 46 | margin-top: 1em; 47 | .control { 48 | margin-top: 1em; 49 | } 50 | } 51 | } 52 | 53 | 54 | #exam-progress { 55 | width: 100%; 56 | .progress-bar { 57 | margin-right: 2em; 58 | vertical-align: middle; 59 | width: 84%; 60 | } 61 | .progress-text { 62 | margin-left: auto; 63 | } 64 | } 65 | 66 | #score-display { 67 | margin-left: auto; 68 | } 69 | 70 | #task-token-preview { 71 | overflow: hidden; 72 | width: 40em; 73 | transition: 1s; 74 | display: inline-block; 75 | } 76 | 77 | #task-token-preview { 78 | overflow: hidden; 79 | width: 40em; 80 | transition: 1s; 81 | display: inline-block; 82 | } 83 | 84 | 85 | @import '../../node_modules/bulma/bulma.sass'; 86 | @import "../../node_modules/bootstrap/scss/bootstrap.scss"; 87 | -------------------------------------------------------------------------------- /src/frontend/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import './globals.scss' 3 | 4 | export const metadata: Metadata = { 5 | title: 'AWS Step Functions Plagiarism Demo - Test Centre', 6 | description: '', 7 | } 8 | 9 | export default function RootLayout({ 10 | children, 11 | }: { 12 | children: React.ReactNode 13 | }) { 14 | return ( 15 | 16 | 17 | {children} 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/frontend/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import ExamPage from './testcentre/page'; 2 | 3 | const HomePage = () => { 4 | return () 5 | } 6 | 7 | export default HomePage; -------------------------------------------------------------------------------- /src/frontend/src/app/testcentre/components/Exam.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { FormEvent, useState } from 'react' 4 | import QuestionData from './QuestionData.json' 5 | import Question from './Question' 6 | 7 | type ExamProps = { 8 | examSubmitted: boolean; 9 | setScore: (score: number) => void; 10 | score: number; 11 | } 12 | 13 | type QuestionType = { 14 | text: string; 15 | answers: object; 16 | correct_answer: string; 17 | question_id: string; 18 | } 19 | 20 | export default function Exam({ examSubmitted, setScore, score }: ExamProps) { 21 | const [questionsAnswered, setQuestionsAnswered] = useState(0); 22 | const totalQuestions: number = QuestionData.questions.length 23 | const [answers, setAnswers] = useState>(new Map()); 24 | 25 | /** 26 | * when a form is submitted. Calculates score and puts it back into the workflow. 27 | */ 28 | function scoreExam(e: FormEvent) { 29 | e.preventDefault(); 30 | // Let other components know what the score is. 31 | // First, collate all the correct answers in one place. 32 | let correctAnswers = new Map(); 33 | QuestionData.questions.forEach((question: QuestionType) => { 34 | correctAnswers.set(question.question_id, question.correct_answer); 35 | }); 36 | 37 | // Then, iterate through the students answers to see how they did. 38 | // Keep a running total of how many answers were answered correctly. 39 | let correctCount = 0; 40 | answers.forEach((answerGiven, questionId) => { 41 | if (correctAnswers.get(questionId) === answerGiven) { 42 | correctCount++; 43 | } 44 | }); 45 | // Record a score out of 100. 46 | const score = correctCount / QuestionData.questions.length * 100; 47 | setScore(score) 48 | } 49 | 50 | /** 51 | * Reacts to a Question select event. 52 | */ 53 | function recordAnswer(questionId: string, answerIdGiven: string) { 54 | console.log(questionId, answerIdGiven); 55 | // Update the progress bar if this question has not already been answered. 56 | if (!answers.get(questionId)) { 57 | setQuestionsAnswered(questionsAnswered + 1); 58 | } 59 | // Record a student answer for later comparison. 60 | setAnswers(answers => answers.set(questionId, answerIdGiven)); 61 | } 62 | 63 | 64 | 65 | return ( 66 |
67 |
scoreExam(e)}> 68 |
69 |

Exam Attempt

70 |
71 |
72 |
73 | 15% 74 |
75 |
76 | Progress 77 | {questionsAnswered} / {totalQuestions} 78 |
79 |
80 |
81 | 82 |
83 | {QuestionData.questions.map((question: QuestionType, index) => ( 84 | 93 | )) 94 | } 95 |
96 | 97 |
98 |
99 | 100 |
101 |
102 |
103 | scoreExam(e)}>Your Score 104 | {score}% 105 |
106 |
107 |
108 |
109 |
110 |
111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /src/frontend/src/app/testcentre/components/ExamIntegration.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useEffect, useState } from 'react' 4 | import { ExamData, submitExam } from '@/api/api' 5 | import { useSearchParams } from 'next/navigation'; 6 | 7 | type ExamIntegrationProps = { 8 | setExamSubmitted: (isExamSubmitted: boolean) => void 9 | score: number 10 | } 11 | 12 | export default function ExamIntegration({ score, setExamSubmitted }: ExamIntegrationProps) { 13 | const [examData, setExamData] = useState({ 14 | // Exam score (out of 100). 15 | Score: score, 16 | // Unique identifier for plagiarism incident. 17 | IncidentId: 'Not supplied', 18 | // Unique identifier for exam attempt. 19 | ExamId: 'Not supplied', 20 | // Task Token unique to the current Step Functions execution. 21 | TaskToken: 'Not supplied' 22 | }); 23 | const [isSubmitSuccessful, setIsSubmitSuccessful] = useState(false); 24 | const [submitMessage, setSubmitMessage] = useState(''); 25 | 26 | const params = useSearchParams(); 27 | 28 | useEffect(() => { 29 | // If present, get the primary keys, then set our hidden form value. 30 | // Keys can get passed as a GET variables, 31 | // in the form ?/TaskToken=baz&IncidentId=foo&ExamId=bar 32 | const incidentId = params.get('IncidentId') || 'Not supplied'; 33 | const examId = params.get('ExamId') || 'Not supplied'; 34 | const taskToken = params.get('TaskToken')?.replaceAll(" ", "+") || 'Not supplied'; 35 | setExamData((examData) => ({ ...examData, IncidentId: incidentId, ExamId: examId, TaskToken: taskToken })); 36 | }, [params]); 37 | 38 | // Add this useEffect to update the score 39 | useEffect(() => { 40 | setExamData((examData) => ({ ...examData, Score: score })); 41 | }, [score]); 42 | 43 | function submitToStepFunctions(event: any) { 44 | event.preventDefault(); 45 | // Post our response back to Step Functions to continue the flow. 46 | submitExam(examData).then(response => { 47 | setIsSubmitSuccessful(true); 48 | console.log(response); 49 | setExamSubmitted(true); 50 | setSubmitMessage('Your exam has been submitted successfully.'); 51 | }).catch(error => { 52 | // Something went wrong. 53 | setIsSubmitSuccessful(true); 54 | setSubmitMessage(error.message || 'Your exam has been submitted successfully, with errors.'); 55 | console.error('Error submitting exam:', error); 56 | }); 57 | } 58 | 59 | return ( 60 |
61 |
62 |
63 |

Test Step Functions Integration

64 |
65 |
66 |
67 | Incident ID 68 | {examData.IncidentId} 69 |
70 |
71 |
72 |
73 | Exam ID 74 | {examData.ExamId} 75 |
76 |
77 |
78 |
79 | Task Token 80 | {/* */} 81 | {examData.TaskToken} 82 |
83 |
84 |
85 |
86 |

87 | This application expects three GET variables to be passed in the URL:

88 |
    89 |
  • IncidentId - the Incident this attempt relates to.
  • 90 |
  • ExamId - the unique identifier for this particular attempt.
  • 91 |
  • TaskToken - a callback task token for this Step Function execution.
  • 92 |
93 | 94 |

95 | You can click the button below to pass some dummy variables, to check the integration is working. 96 |

97 |
98 | 99 |
100 |
101 | 105 | 106 |
submitToStepFunctions(e)}> 107 | Submit to Step Function Execution 108 |
109 |
110 |
111 | 112 | {submitMessage.length > 0 && ( 113 | isSubmitSuccessful ? ( 114 |
115 | 116 | Your exam has been submitted. Your score was {examData.Score}% Your assessor will be in touch to let you 117 | know what the next steps are. 118 |
119 | ) : ( 120 |
121 | 122 |

There was an error when submitting your exam:

123 |
124 |
{submitMessage}
125 |
126 | ) 127 | )} 128 |
129 |
130 | 131 | ); 132 | } 133 | 134 | 135 | -------------------------------------------------------------------------------- /src/frontend/src/app/testcentre/components/Question.tsx: -------------------------------------------------------------------------------- 1 | type QuestionProps = { 2 | recordAnswer: (questionId: string, answer: string) => void; 3 | questionText: string; 4 | answers: Object; 5 | questionId: string; 6 | questionNumber: number; 7 | disabled: boolean; 8 | } 9 | 10 | export default function Question({ recordAnswer, questionText, answers, questionId, questionNumber, disabled }: QuestionProps) { 11 | 12 | return (
13 |
14 | 18 |
19 |
20 | 35 |
36 |
37 |

Choose the best option.

38 |
39 | 40 |
); 41 | } -------------------------------------------------------------------------------- /src/frontend/src/app/testcentre/components/questionData.json: -------------------------------------------------------------------------------- 1 | { 2 | "questions" : [ 3 | { 4 | "text": "What is Plagiarism?", 5 | "answers": { 6 | "q1a": "Borrowing an original idea and presenting it as a new idea", 7 | "q1b": "A way of writing", 8 | "q1c": "A new type of tropical disease", 9 | "q1d": "Ethical behavior" 10 | }, 11 | "correct_answer": "q1a", 12 | "question_id": "q1" 13 | }, 14 | { 15 | "text": "Is it acceptable to copy-and-paste a sentence written by someone else into your paper and simply add quotation marks around it?", 16 | "answers": { 17 | "q2a": "Yes, that shows it is not original text", 18 | "q2b": "No, that is incomplete citation" 19 | }, 20 | "correct_answer": "q2b", 21 | "question_id": "q2" 22 | }, 23 | { 24 | "text": "Paraphrasing properly is to:", 25 | "answers": { 26 | "q3q": "Change a few words to make it your own and cite it", 27 | "q3b": "Put quotation marks around the text and cite it", 28 | "q3c": "Use only the idea without citing it", 29 | "q3d": "Summarize the text in your own words and cite it" 30 | }, 31 | "correct_answer": "q3d", 32 | "question_id": "q3" 33 | }, 34 | { 35 | "text": "You re-use paragraphs from a paper you wrote last semester and put it into a new assignment, and you don’t cite it because it’s your own work. Is this plagiarism?", 36 | "answers": { 37 | "q4a": "Yes, it is self-plagiarism", 38 | "q4b": "No, it isn't plagiarism" 39 | }, 40 | "correct_answer": "q4a", 41 | "question_id": "q4" 42 | }, 43 | { 44 | "text": "A source doesn’t need to be cited if it’s collaboratively written on the web like Wikipedia", 45 | "answers": { 46 | "q5a": "True", 47 | "q5b": "False" 48 | }, 49 | "correct_answer": "q5b", 50 | "question_id": "q5" 51 | } 52 | ] 53 | } -------------------------------------------------------------------------------- /src/frontend/src/app/testcentre/custom.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | // Colours. 4 | // Taken from AWS Brand Book (July 2018). 5 | // Primary brand colour 6 | $aws_orange: #FF9900; 7 | // Primary neutral brand colour. 8 | $aws_squidink: #232F3E; 9 | //$heading-color: rgb(35, 47, 62); // this is the same as squidink 10 | //$aws_bg_grey: #f7f7f7; 11 | // Secondary Data. 12 | $aws_mercury: #545b64; 13 | $white: #FFF; 14 | 15 | // Customizing Bulma variables. 16 | $dark: $aws_squidink; 17 | 18 | // Images. 19 | // @todo investigate file-loader to avoid keeping images in /public. 20 | $asset_path: "/images"; 21 | $aws_logo_img: "#{$asset_path}/aws-smile.png"; 22 | $aws_hero_bg_img: "#{$asset_path}/bg.png"; 23 | 24 | // Main title 25 | $title_color: $aws_squidink; 26 | 27 | // Navbar 28 | $navbar-background-color: $aws_squidink; 29 | 30 | #exam-hero { 31 | background-image: 32 | url($aws_logo_img), 33 | url($aws_hero_bg_img); 34 | background-position: 35 | right 1em top 1em, 36 | right; 37 | background-repeat: 38 | no-repeat, 39 | no-repeat; 40 | margin-bottom: -1.5em; 41 | } 42 | 43 | .exam { 44 | margin-bottom: 1em; 45 | .question, 46 | .submit { 47 | margin-top: 1em; 48 | .control { 49 | margin-top: 1em; 50 | } 51 | } 52 | } 53 | 54 | 55 | #exam-progress { 56 | width: 100%; 57 | .progress-bar { 58 | margin-right: 2em; 59 | vertical-align: middle; 60 | width: 84%; 61 | } 62 | .progress-text { 63 | margin-left: auto; 64 | } 65 | } 66 | 67 | #score-display { 68 | margin-left: auto; 69 | } 70 | 71 | #task-token-preview { 72 | overflow: hidden; 73 | width: 40em; 74 | transition: 1s; 75 | } 76 | 77 | #task-token-preview:hover { 78 | text-wrap: pretty; 79 | } 80 | -------------------------------------------------------------------------------- /src/frontend/src/app/testcentre/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Exam from '@/app/testcentre/components/Exam' 4 | import { useState } from 'react' 5 | import ExamIntegration from './components/ExamIntegration' 6 | 7 | 8 | export default function ExamPage() { 9 | const [examSubmitted, setExamSubmitted] = useState(false) 10 | const [score, setScore] = useState(0) 11 | return ( 12 |
13 |
14 |
15 |

AWS Step Functions Plagiarism Exam

16 |

17 | Please re-attempt the exam. Once you are done, submit your exam for marking. 18 |

19 |
20 |
21 |
22 |
23 |
24 | 25 | 26 |
27 |
28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/frontend/testcentre-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-step-functions-plagiarism-demo-dotnet/5514aaa30ca5c5cc563f3fbf017de5117c1dae99/src/frontend/testcentre-screenshot.png -------------------------------------------------------------------------------- /src/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------