diff --git a/.env.example b/.env.example index 42dcfe76..ad45d3e5 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,7 @@ SHAPE_DOCS_BASE_URL=http://localhost:3000 SHAPE_DOCS_PROJECT_CONFIGURATION_FILENAME=.shape-docs.yml NEXT_PUBLIC_SHAPE_DOCS_TITLE=Shape Docs NEXT_PUBLIC_SHAPE_DOCS_DESCRIPTION=Documentation for Shape's APIs +NEXT_PUBLIC_SHAPE_DOCS_HELP_URL=https://github.com/shapehq/shape-docs/wiki NEXTAUTH_URL_INTERNAL=http://localhost:3000 NEXTAUTH_SECRET=use [openssl rand -base64 32] to generate a 32 bytes value REDIS_URL=localhost @@ -10,6 +11,8 @@ POSTGRESQL_USER=dbuser POSTGRESQL_PASSWORD= POSTGRESQL_DB=shape-docs REPOSITORY_NAME_SUFFIX=-openapi +HIDDEN_REPOSITORIES= +NEW_PROJECT_TEMPLATE_REPOSITORY=shapehq/starter-openapi GITHUB_WEBHOOK_SECRET=preshared secret also put in app configuration in GitHub GITHUB_WEBHOK_REPOSITORY_ALLOWLIST= GITHUB_WEBHOK_REPOSITORY_DISALLOWLIST= diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fe03f521..cda52edb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,3 +11,6 @@ updates: octokit: patterns: - "@octokit/*" + mui: + patterns: + - "@mui/*" diff --git a/__test__/auth/AuthjsAccountsOAuthTokenRepository.test.ts b/__test__/auth/AuthjsAccountsOAuthTokenRepository.test.ts index 685c2c02..f31b7e7f 100644 --- a/__test__/auth/AuthjsAccountsOAuthTokenRepository.test.ts +++ b/__test__/auth/AuthjsAccountsOAuthTokenRepository.test.ts @@ -1,4 +1,4 @@ -import { AuthjsAccountsOAuthTokenRepository } from "../../src/features/auth/domain" +import { AuthjsAccountsOAuthTokenRepository } from "@/features/auth/domain" test("It gets token for user ID and provider", async () => { let queryUserId: string | undefined diff --git a/__test__/auth/CompositeLogOutHandler.test.ts b/__test__/auth/CompositeLogOutHandler.test.ts index f95757bf..83c00fe1 100644 --- a/__test__/auth/CompositeLogOutHandler.test.ts +++ b/__test__/auth/CompositeLogOutHandler.test.ts @@ -1,4 +1,4 @@ -import { CompositeLogOutHandler } from "../../src/features/auth/domain" +import { CompositeLogOutHandler } from "@/features/auth/domain" test("It invokes all log out handlers", async () => { let didCallLogOutHandler1 = false diff --git a/__test__/auth/ErrorIgnoringLogOutHandler.test.ts b/__test__/auth/ErrorIgnoringLogOutHandler.test.ts index a7bacdd0..d6375f3f 100644 --- a/__test__/auth/ErrorIgnoringLogOutHandler.test.ts +++ b/__test__/auth/ErrorIgnoringLogOutHandler.test.ts @@ -1,4 +1,4 @@ -import { ErrorIgnoringLogOutHandler } from "../../src/features/auth/domain" +import { ErrorIgnoringLogOutHandler } from "@/features/auth/domain" test("It ignores errors", async () => { const sut = new ErrorIgnoringLogOutHandler({ diff --git a/__test__/auth/LockingAccessTokenRefresher.test.ts b/__test__/auth/LockingAccessTokenRefresher.test.ts index 00ee777f..736f519c 100644 --- a/__test__/auth/LockingAccessTokenRefresher.test.ts +++ b/__test__/auth/LockingAccessTokenRefresher.test.ts @@ -1,4 +1,4 @@ -import { LockingOAuthTokenRefresher } from "../../src/features/auth/domain" +import { LockingOAuthTokenRefresher } from "@/features/auth/domain" test("It acquires a lock", async () => { let didAcquireLock = false diff --git a/__test__/auth/LogInHandler.test.ts b/__test__/auth/LogInHandler.test.ts index d1dd0c0f..05f87227 100644 --- a/__test__/auth/LogInHandler.test.ts +++ b/__test__/auth/LogInHandler.test.ts @@ -1,4 +1,4 @@ -import { LogInHandler } from "../../src/features/auth/domain" +import { LogInHandler } from "@/features/auth/domain" test("It disallows logging in when account is undefined", async () => { const sut = new LogInHandler({ diff --git a/__test__/auth/OAuthTokenDataSource.test.ts b/__test__/auth/OAuthTokenDataSource.test.ts index 28390b61..857b4e8e 100644 --- a/__test__/auth/OAuthTokenDataSource.test.ts +++ b/__test__/auth/OAuthTokenDataSource.test.ts @@ -1,4 +1,4 @@ -import { OAuthTokenDataSource } from "../../src/features/auth/domain" +import { OAuthTokenDataSource } from "@/features/auth/domain" test("It reads OAuth token for user's ID", async () => { let readUserId: string | undefined diff --git a/__test__/auth/OAuthTokenRepository.test.ts b/__test__/auth/OAuthTokenRepository.test.ts index e7f1a124..bf2c964f 100644 --- a/__test__/auth/OAuthTokenRepository.test.ts +++ b/__test__/auth/OAuthTokenRepository.test.ts @@ -1,4 +1,4 @@ -import { OAuthTokenRepository } from "../../src/features/auth/domain" +import { OAuthTokenRepository } from "@/features/auth/domain" test("It reads the auth token for the specified user", async () => { let readProvider: string | undefined diff --git a/__test__/auth/OAuthTokenSessionValidator.test.ts b/__test__/auth/OAuthTokenSessionValidator.test.ts index 2abadbbd..60f75e3f 100644 --- a/__test__/auth/OAuthTokenSessionValidator.test.ts +++ b/__test__/auth/OAuthTokenSessionValidator.test.ts @@ -1,4 +1,4 @@ -import { OAuthTokenSessionValidator, SessionValidity } from "../../src/features/auth/domain" +import { OAuthTokenSessionValidator, SessionValidity } from "@/features/auth/domain" test("It reads the access token", async () => { let didReadOAuthToken = false diff --git a/__test__/auth/PersistingOAuthTokenRefresher.test.ts b/__test__/auth/PersistingOAuthTokenRefresher.test.ts index a2537939..791c1b0c 100644 --- a/__test__/auth/PersistingOAuthTokenRefresher.test.ts +++ b/__test__/auth/PersistingOAuthTokenRefresher.test.ts @@ -1,4 +1,4 @@ -import { PersistingOAuthTokenRefresher, OAuthToken } from "../../src/features/auth/domain" +import { PersistingOAuthTokenRefresher, OAuthToken } from "@/features/auth/domain" test("It refreshes OAuth token using provided refresh token", async () => { let usedRefreshToken: string | undefined diff --git a/__test__/auth/UserDataCleanUpLogOutHandler.test.ts b/__test__/auth/UserDataCleanUpLogOutHandler.test.ts index 6cbfc03d..9b7ec310 100644 --- a/__test__/auth/UserDataCleanUpLogOutHandler.test.ts +++ b/__test__/auth/UserDataCleanUpLogOutHandler.test.ts @@ -1,4 +1,4 @@ -import { UserDataCleanUpLogOutHandler } from "../../src/features/auth/domain" +import { UserDataCleanUpLogOutHandler } from "@/features/auth/domain" test("It deletes data for the read user ID", async () => { let deletedUserId: string | undefined diff --git a/__test__/common/github/OAuthTokenRefreshingGitHubClient.test.ts b/__test__/common/github/OAuthTokenRefreshingGitHubClient.test.ts index 7e353354..548d85a3 100644 --- a/__test__/common/github/OAuthTokenRefreshingGitHubClient.test.ts +++ b/__test__/common/github/OAuthTokenRefreshingGitHubClient.test.ts @@ -1,10 +1,10 @@ -import { OAuthTokenRefreshingGitHubClient } from "@/common" import { + OAuthTokenRefreshingGitHubClient, GraphQLQueryRequest, GetRepositoryContentRequest, GetPullRequestCommentsRequest, AddCommentToPullRequestRequest -} from "@/common/github/IGitHubClient" +} from "@/common" test("It forwards a GraphQL request", async () => { let forwardedRequest: GraphQLQueryRequest | undefined diff --git a/__test__/common/utils/listFromCommaSeparatedString.test.ts b/__test__/common/utils/listFromCommaSeparatedString.test.ts index e3f75d43..08fad85b 100644 --- a/__test__/common/utils/listFromCommaSeparatedString.test.ts +++ b/__test__/common/utils/listFromCommaSeparatedString.test.ts @@ -1,4 +1,4 @@ -import listFromCommaSeparatedString from "@/common/utils/listFromCommaSeparatedString" +import { listFromCommaSeparatedString } from "@/common" test("It returns an empty list given undefined", async () => { const result = listFromCommaSeparatedString(undefined) diff --git a/__test__/common/utils/saneParseInt.test.ts b/__test__/common/utils/saneParseInt.test.ts index ebd217d4..5acc1e28 100644 --- a/__test__/common/utils/saneParseInt.test.ts +++ b/__test__/common/utils/saneParseInt.test.ts @@ -1,4 +1,4 @@ -import saneParseInt from "@/common/utils/saneParseInt" +import { saneParseInt } from "@/common" test("It parses an integer", async () => { // @ts-ignore diff --git a/__test__/hooks/FilteringPullRequestEventHandler.test.ts b/__test__/hooks/FilteringPullRequestEventHandler.test.ts index 2c1e8045..65b5836f 100644 --- a/__test__/hooks/FilteringPullRequestEventHandler.test.ts +++ b/__test__/hooks/FilteringPullRequestEventHandler.test.ts @@ -1,4 +1,4 @@ -import { FilteringPullRequestEventHandler } from "../../src/features/hooks/domain" +import { FilteringPullRequestEventHandler } from "@/features/hooks/domain" test("It calls pullRequestOpened(_:) when event is included", async () => { let didCall = false diff --git a/__test__/hooks/PostCommentPullRequestEventHandler.test.ts b/__test__/hooks/PostCommentPullRequestEventHandler.test.ts index 46f7ee55..153c3fa7 100644 --- a/__test__/hooks/PostCommentPullRequestEventHandler.test.ts +++ b/__test__/hooks/PostCommentPullRequestEventHandler.test.ts @@ -1,4 +1,4 @@ -import { PostCommentPullRequestEventHandler } from "../../src/features/hooks/domain" +import { PostCommentPullRequestEventHandler } from "@/features/hooks/domain" test("It comments when opening a pull request", async () => { let didComment = false diff --git a/__test__/hooks/PullRequestCommenter.test.ts b/__test__/hooks/PullRequestCommenter.test.ts index 358c5af5..ff4cadde 100644 --- a/__test__/hooks/PullRequestCommenter.test.ts +++ b/__test__/hooks/PullRequestCommenter.test.ts @@ -1,4 +1,4 @@ -import { PullRequestCommenter } from "../../src/features/hooks/domain" +import { PullRequestCommenter } from "@/features/hooks/domain" test("It adds comment when none exist", async () => { let didAddComment = false diff --git a/__test__/hooks/RepositoryNameEventFilter.test.ts b/__test__/hooks/RepositoryNameEventFilter.test.ts index 473ce34b..2b06ca1d 100644 --- a/__test__/hooks/RepositoryNameEventFilter.test.ts +++ b/__test__/hooks/RepositoryNameEventFilter.test.ts @@ -1,4 +1,4 @@ -import { RepositoryNameEventFilter } from "../../src/features/hooks/domain" +import { RepositoryNameEventFilter } from "@/features/hooks/domain" test("It does not include repositories that do not have the \"-openapi\" suffix", async () => { const sut = new RepositoryNameEventFilter({ diff --git a/__test__/projects/CachingProjectDataSource.test.ts b/__test__/projects/CachingProjectDataSource.test.ts index 0a9fc418..9365f53a 100644 --- a/__test__/projects/CachingProjectDataSource.test.ts +++ b/__test__/projects/CachingProjectDataSource.test.ts @@ -1,4 +1,4 @@ -import { Project, CachingProjectDataSource } from "../../src/features/projects/domain" +import { Project, CachingProjectDataSource } from "@/features/projects/domain" test("It caches projects read from the data source", async () => { const projects: Project[] = [{ diff --git a/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts b/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts new file mode 100644 index 00000000..e834d7b0 --- /dev/null +++ b/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts @@ -0,0 +1,125 @@ +import { FilteringGitHubRepositoryDataSource } from "@/features/projects/domain" + +test("It returns all repositories when no hidden repositories are provided", async () => { + const sut = new FilteringGitHubRepositoryDataSource({ + hiddenRepositories: [], + dataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [], + tags: [] + }, { + owner: "acme", + name: "bar-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + branches: [], + tags: [] + }] + } + } + }) + const repositories = await sut.getRepositories() + expect(repositories.length).toEqual(2) +}) + +test("It removes hidden repository", async () => { + const sut = new FilteringGitHubRepositoryDataSource({ + hiddenRepositories: ["acme/foo-openapi"], + dataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [], + tags: [] + }, { + owner: "acme", + name: "bar-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + branches: [], + tags: [] + }] + } + } + }) + const repositories = await sut.getRepositories() + expect(repositories.length).toEqual(1) +}) + +test("It returns unmodified list when hidden repository was not found", async () => { + const sut = new FilteringGitHubRepositoryDataSource({ + hiddenRepositories: ["acme/baz-openapi"], + dataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [], + tags: [] + }, { + owner: "acme", + name: "bar-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + branches: [], + tags: [] + }] + } + } + }) + const repositories = await sut.getRepositories() + expect(repositories.length).toEqual(2) +}) + +test("It removes multiple hidden repositories", async () => { + const sut = new FilteringGitHubRepositoryDataSource({ + hiddenRepositories: ["acme/foo-openapi", "acme/bar-openapi"], + dataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [], + tags: [] + }, { + owner: "acme", + name: "bar-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + branches: [], + tags: [] + }] + } + } + }) + const repositories = await sut.getRepositories() + expect(repositories.length).toEqual(0) +}) diff --git a/__test__/projects/GitHubProjectDataSource.test.ts b/__test__/projects/GitHubProjectDataSource.test.ts index b4aeb857..ac01eaa1 100644 --- a/__test__/projects/GitHubProjectDataSource.test.ts +++ b/__test__/projects/GitHubProjectDataSource.test.ts @@ -1,25 +1,13 @@ -import { - GitHubProjectDataSource - } from "../../src/features/projects/data" +import { GitHubProjectDataSource } from "@/features/projects/data" test("It loads repositories from data source", async () => { let didLoadRepositories = false const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { + repositoryDataSource: { + async getRepositories() { didLoadRepositories = true - return { - search: { - results: [] - } - } + return [] } } }) @@ -30,60 +18,30 @@ test("It loads repositories from data source", async () => { test("It maps projects including branches and tags", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [{ - node: { - name: "1.0", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml" }] - } - } + }], + tags: [{ + id: "12345678", + name: "1.0", + files: [{ + name: "openapi.yml" + }] + }] + }] } } }) @@ -121,63 +79,33 @@ test("It maps projects including branches and tags", async () => { }]) }) -test("It removes \"-openapi\" suffix from project name", async () => { +test("It removes suffix from project name", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [{ - node: { - name: "1.0", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml" }] - } - } + }], + tags: [{ + id: "12345678", + name: "1.0", + files: [{ + name: "openapi.yml" + }] + }] + }] } } }) @@ -190,64 +118,34 @@ test("It removes \"-openapi\" suffix from project name", async () => { test("It supports multiple OpenAPI specifications on a branch", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "foo-service.yml" - }, { - name: "bar-service.yml" - }, { - name: "baz-service.yml" - }] - } - } - } - }] - }, - tags: { - edges: [{ - node: { - name: "1.0", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "foo-service.yml", + }, { + name: "bar-service.yml", + }, { + name: "baz-service.yml", }] - } - } + }], + tags: [{ + id: "12345678", + name: "1.0", + files: [{ + name: "openapi.yml" + }] + }] + }] } } }) @@ -295,105 +193,21 @@ test("It supports multiple OpenAPI specifications on a branch", async () => { }]) }) -test("It removes \"-openapi\" suffix from project name", async () => { - const sut = new GitHubProjectDataSource({ - repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [{ - node: { - name: "1.0", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - } - }] - } - } - } - } - }) - const projects = await sut.getProjects() - expect(projects[0].id).toEqual("acme-foo") - expect(projects[0].name).toEqual("foo") - expect(projects[0].displayName).toEqual("foo") -}) - test("It filters away projects with no versions", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [] - }, - tags: { - edges: [] - } - }] - } - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [], + tags: [] + }] } } }) @@ -404,60 +218,30 @@ test("It filters away projects with no versions", async () => { test("It filters away branches with no specifications", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "bugfix", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "foo.txt" - }] - } - } - } - }] - }, - tags: { - edges: [] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", }] - } - } + }, { + id: "12345678", + name: "bugfix", + files: [{ + name: "README.md", + }] + }], + tags: [] + }] } } }) @@ -468,72 +252,36 @@ test("It filters away branches with no specifications", async () => { test("It filters away tags with no specifications", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [{ - node: { - name: "1.0", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "1.1", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "foo.txt" - }] - } - } - } - }] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "foo-service.yml", }] - } - } + }], + tags: [{ + id: "12345678", + name: "1.0", + files: [{ + name: "openapi.yml" + }] + }, { + id: "12345678", + name: "0.1", + files: [{ + name: "README.md" + }] + }] + }] } } }) @@ -544,51 +292,27 @@ test("It filters away tags with no specifications", async () => { test("It reads image from configuration file with .yml extension", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - configYml: { - text: "image: icon.png" - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYml: { + text: "image: icon.png" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", }] - } - } + }], + tags: [] + }] } } }) @@ -596,130 +320,30 @@ test("It reads image from configuration file with .yml extension", async () => { expect(projects[0].imageURL).toEqual("/api/blob/acme/foo-openapi/icon.png?ref=12345678") }) -test("It filters away tags with no specifications", async () => { - const sut = new GitHubProjectDataSource({ - repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [{ - node: { - name: "1.0", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "1.1", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "foo.txt" - }] - } - } - } - }] - } - }] - } - } - } - } - }) - const projects = await sut.getProjects() - expect(projects[0].versions.length).toEqual(2) -}) - test("It reads display name from configuration file with .yml extension", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - configYml: { - text: "name: Hello World" - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYml: { + text: "name: Hello World" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", }] - } - } + }], + tags: [] + }] } } }) @@ -729,54 +353,30 @@ test("It reads display name from configuration file with .yml extension", async expect(projects[0].displayName).toEqual("Hello World") }) -test("It reads image from configuration file with .yml extension", async () => { +test("It reads image from configuration file with .yaml extension", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - configYml: { - text: "image: icon.png" - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYaml: { + text: "image: icon.png" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", }] - } - } + }], + tags: [] + }] } } }) @@ -787,51 +387,27 @@ test("It reads image from configuration file with .yml extension", async () => { test("It reads display name from configuration file with .yaml extension", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - configYaml: { - text: "name: Hello World" - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYaml: { + text: "name: Hello World" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", }] - } - } + }], + tags: [] + }] } } }) @@ -841,164 +417,66 @@ test("It reads display name from configuration file with .yaml extension", async expect(projects[0].displayName).toEqual("Hello World") }) -test("It reads image from configuration file with .yaml extension", async () => { - const sut = new GitHubProjectDataSource({ - repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - configYaml: { - text: "image: icon.png" - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } - }] - } - } - } - } - }) - const projects = await sut.getProjects() - expect(projects[0].imageURL).toEqual("/api/blob/acme/foo-openapi/icon.png?ref=12345678") -}) - test("It sorts projects alphabetically", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "cathrine-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } - }, { - name: "anne-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } - }, { - name: "bobby-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "cathrine-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYaml: { + text: "name: Hello World" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", + }] + }], + tags: [] + }, { + owner: "acme", + name: "bobby-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYaml: { + text: "name: Hello World" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", + }] + }], + tags: [] + }, { + owner: "acme", + name: "anne-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYaml: { + text: "name: Hello World" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", }] - } - } + }], + tags: [] + }] } } }) @@ -1011,84 +489,45 @@ test("It sorts projects alphabetically", async () => { test("It sorts versions alphabetically", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "bobby", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "anne", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [{ - node: { - name: "1.0", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "cathrine", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYaml: { + text: "name: Hello World" + }, + branches: [{ + id: "12345678", + name: "anne", + files: [{ + name: "openapi.yml", + }] + }, { + id: "12345678", + name: "bobby", + files: [{ + name: "openapi.yml", + }] + }], + tags: [{ + id: "12345678", + name: "cathrine", + files: [{ + name: "openapi.yml", + }] + }, { + id: "12345678", + name: "1.0", + files: [{ + name: "openapi.yml", }] - } - } + }] + }] } } }) @@ -1102,108 +541,57 @@ test("It sorts versions alphabetically", async () => { test("It prioritizes main, master, develop, and development branch names when sorting verisons", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "anne", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "develop", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "development", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "master", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [{ - node: { - name: "1.0", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYaml: { + text: "name: Hello World" + }, + branches: [{ + id: "12345678", + name: "anne", + files: [{ + name: "openapi.yml", + }] + }, { + id: "12345678", + name: "develop", + files: [{ + name: "openapi.yml", + }] + }, { + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", + }] + }, { + id: "12345678", + name: "development", + files: [{ + name: "openapi.yml", }] - } - } + }, { + id: "12345678", + name: "master", + files: [{ + name: "openapi.yml", + }] + }], + tags: [{ + id: "12345678", + name: "1.0", + files: [{ + name: "openapi.yml", + }] + }] + }] } } }) @@ -1219,72 +607,39 @@ test("It prioritizes main, master, develop, and development branch names when so test("It identifies the default branch in returned versions", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "development", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "anne", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "development", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "development" + }, + configYaml: { + text: "name: Hello World" + }, + branches: [{ + id: "12345678", + name: "anne", + files: [{ + name: "openapi.yml", }] - } - } + }, { + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", + }] + }, { + id: "12345678", + name: "development", + files: [{ + name: "openapi.yml", + }] + }], + tags: [] + }] } } }) @@ -1297,54 +652,35 @@ test("It identifies the default branch in returned versions", async () => { }) test("It adds remote versions from the project configuration", async () => { - const rawProjectConfig = ` - remoteVersions: - - name: Anne - specifications: - - name: Huey - url: https://example.com/huey.yml - - name: Dewey - url: https://example.com/dewey.yml - - name: Bobby - specifications: - - name: Louie - url: https://example.com/louie.yml - ` const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - configYml: { - text: rawProjectConfig - }, - branches: { - edges: [] - }, - tags: { - edges: [] - } - }] - } - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYaml: { + text: ` + remoteVersions: + - name: Anne + specifications: + - name: Huey + url: https://example.com/huey.yml + - name: Dewey + url: https://example.com/dewey.yml + - name: Bobby + specifications: + - name: Louie + url: https://example.com/louie.yml + ` + }, + branches: [], + tags: [] + }] } } }) @@ -1375,64 +711,39 @@ test("It adds remote versions from the project configuration", async () => { }) test("It modifies ID of remote version if the ID already exists", async () => { - const rawProjectConfig = ` - remoteVersions: - - name: Bar - specifications: - - name: Baz - url: https://example.com/baz.yml - - name: Bar - specifications: - - name: Hello - url: https://example.com/hello.yml - ` const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "bar", - target: { - oid: "12345678" - } - }, - configYml: { - text: rawProjectConfig - }, - branches: { - edges: [{ - node: { - name: "bar", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + configYaml: { + text: ` + remoteVersions: + - name: Bar + specifications: + - name: Baz + url: https://example.com/baz.yml + - name: Bar + specifications: + - name: Hello + url: https://example.com/hello.yml + ` + }, + branches: [{ + id: "12345678", + name: "bar", + files: [{ + name: "openapi.yml" }] - } - } + }], + tags: [] + }] } } }) @@ -1470,49 +781,30 @@ test("It modifies ID of remote version if the ID already exists", async () => { }) test("It lets users specify the ID of a remote version", async () => { - const rawProjectConfig = ` - remoteVersions: - - id: some-version - name: Bar - specifications: - - name: Baz - url: https://example.com/baz.yml - ` const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "bar", - target: { - oid: "12345678" - } - }, - configYml: { - text: rawProjectConfig - }, - branches: { - edges: [] - }, - tags: { - edges: [] - } - }] - } - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + configYaml: { + text: ` + remoteVersions: + - id: some-version + name: Bar + specifications: + - name: Baz + url: https://example.com/baz.yml + ` + }, + branches: [], + tags: [] + }] } } }) @@ -1530,49 +822,30 @@ test("It lets users specify the ID of a remote version", async () => { }) test("It lets users specify the ID of a remote specification", async () => { - const rawProjectConfig = ` - remoteVersions: - - name: Bar - specifications: - - id: some-spec - name: Baz - url: https://example.com/baz.yml - ` const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "bar", - target: { - oid: "12345678" - } - }, - configYml: { - text: rawProjectConfig - }, - branches: { - edges: [] - }, - tags: { - edges: [] - } - }] - } - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + configYaml: { + text: ` + remoteVersions: + - name: Bar + specifications: + - id: some-spec + name: Baz + url: https://example.com/baz.yml + ` + }, + branches: [], + tags: [] + }] } } }) @@ -1588,112 +861,3 @@ test("It lets users specify the ID of a remote specification", async () => { }] }]) }) - -test("It queries for both .yml and .yaml file extension with specifying .yml extension", async () => { - let query: string | undefined - const sut = new GitHubProjectDataSource({ - repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql(request) { - query = request.query - return { - search: { - results: [] - } - } - } - } - }) - await sut.getProjects() - expect(query).toContain(".demo-docs.yml") - expect(query).toContain(".demo-docs.yaml") -}) - -test("It queries for both .yml and .yaml file extension with specifying .yaml extension", async () => { - let query: string | undefined - const sut = new GitHubProjectDataSource({ - repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql(request) { - query = request.query - return { - search: { - results: [] - } - } - } - } - }) - await sut.getProjects() - expect(query).toContain(".demo-docs.yml") - expect(query).toContain(".demo-docs.yaml") -}) - -test("It queries for both .yml and .yaml file extension with no extension", async () => { - let query: string | undefined - const sut = new GitHubProjectDataSource({ - repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql(request) { - query = request.query - return { - search: { - results: [] - } - } - } - } - }) - await sut.getProjects() - expect(query).toContain(".demo-docs.yml") - expect(query).toContain(".demo-docs.yaml") -}) - -test("It loads projects for all logins", async () => { - let searchQueries: string[] = [] - const sut = new GitHubProjectDataSource({ - repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs", - loginsDataSource: { - async getLogins() { - return ["acme", "somecorp", "techsystems"] - } - }, - graphQlClient: { - async graphql(request) { - if (request.variables?.searchQuery) { - searchQueries.push(request.variables.searchQuery) - } - return { - search: { - results: [] - } - } - } - } - }) - await sut.getProjects() - expect(searchQueries.length).toEqual(4) - expect(searchQueries).toContain("\"-openapi\" in:name is:private") - expect(searchQueries).toContain("\"-openapi\" in:name user:acme is:public") - expect(searchQueries).toContain("\"-openapi\" in:name user:somecorp is:public") - expect(searchQueries).toContain("\"-openapi\" in:name user:techsystems is:public") -}) diff --git a/__test__/projects/GitHubRepositoryDataSource.test.ts b/__test__/projects/GitHubRepositoryDataSource.test.ts new file mode 100644 index 00000000..9235a28b --- /dev/null +++ b/__test__/projects/GitHubRepositoryDataSource.test.ts @@ -0,0 +1,220 @@ +import { GitHubRepositoryDataSource } from "@/features/projects/data" + +test("It loads repositories from data source", async () => { + let didLoadRepositories = false + const sut = new GitHubRepositoryDataSource({ + repositoryNameSuffix: "-openapi", + projectConfigurationFilename: ".demo-docs.yml", + loginsDataSource: { + async getLogins() { + return ["acme"] + } + }, + graphQlClient: { + async graphql() { + didLoadRepositories = true + return { + search: { + results: [] + } + } + } + } + }) + await sut.getRepositories() + expect(didLoadRepositories).toBeTruthy() +}) + +test("It maps repositories from GraphQL to the GitHubRepository model", async () => { + const sut = new GitHubRepositoryDataSource({ + repositoryNameSuffix: "-openapi", + projectConfigurationFilename: ".demo-docs.yml", + loginsDataSource: { + async getLogins() { + return ["acme"] + } + }, + graphQlClient: { + async graphql() { + return { + search: { + results: [{ + name: "foo-openapi", + owner: { + login: "acme" + }, + defaultBranchRef: { + name: "main", + target: { + oid: "12345678" + } + }, + branches: { + edges: [{ + node: { + name: "main", + target: { + oid: "12345678", + tree: { + entries: [{ + name: "openapi.yml" + }] + } + } + } + }] + }, + tags: { + edges: [{ + node: { + name: "1.0", + target: { + oid: "12345678", + tree: { + entries: [{ + name: "openapi.yml" + }] + } + } + } + }] + } + }] + } + } + } + } + }) + const repositories = await sut.getRepositories() + expect(repositories).toEqual([{ + name: "foo-openapi", + owner: "acme", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml" + }] + }], + tags: [{ + id: "12345678", + name: "1.0", + files: [{ + name: "openapi.yml" + }] + }] + }]) +}) + +test("It queries for both .yml and .yaml file extension with specifying .yml extension", async () => { + let query: string | undefined + const sut = new GitHubRepositoryDataSource({ + repositoryNameSuffix: "-openapi", + projectConfigurationFilename: ".demo-docs.yml", + loginsDataSource: { + async getLogins() { + return ["acme"] + } + }, + graphQlClient: { + async graphql(request) { + query = request.query + return { + search: { + results: [] + } + } + } + } + }) + await sut.getRepositories() + expect(query).toContain(".demo-docs.yml") + expect(query).toContain(".demo-docs.yaml") +}) + +test("It queries for both .yml and .yaml file extension with specifying .yaml extension", async () => { + let query: string | undefined + const sut = new GitHubRepositoryDataSource({ + repositoryNameSuffix: "-openapi", + projectConfigurationFilename: ".demo-docs.yml", + loginsDataSource: { + async getLogins() { + return ["acme"] + } + }, + graphQlClient: { + async graphql(request) { + query = request.query + return { + search: { + results: [] + } + } + } + } + }) + await sut.getRepositories() + expect(query).toContain(".demo-docs.yml") + expect(query).toContain(".demo-docs.yaml") +}) + +test("It queries for both .yml and .yaml file extension with no extension", async () => { + let query: string | undefined + const sut = new GitHubRepositoryDataSource({ + repositoryNameSuffix: "-openapi", + projectConfigurationFilename: ".demo-docs", + loginsDataSource: { + async getLogins() { + return ["acme"] + } + }, + graphQlClient: { + async graphql(request) { + query = request.query + return { + search: { + results: [] + } + } + } + } + }) + await sut.getRepositories() + expect(query).toContain(".demo-docs.yml") + expect(query).toContain(".demo-docs.yaml") +}) + +test("It loads repositories for all logins", async () => { + let searchQueries: string[] = [] + const sut = new GitHubRepositoryDataSource({ + repositoryNameSuffix: "-openapi", + projectConfigurationFilename: ".demo-docs", + loginsDataSource: { + async getLogins() { + return ["acme", "somecorp", "techsystems"] + } + }, + graphQlClient: { + async graphql(request) { + if (request.variables?.searchQuery) { + searchQueries.push(request.variables.searchQuery) + } + return { + search: { + results: [] + } + } + } + } + }) + await sut.getRepositories() + expect(searchQueries.length).toEqual(4) + expect(searchQueries).toContain("\"-openapi\" in:name is:private") + expect(searchQueries).toContain("\"-openapi\" in:name user:acme is:public") + expect(searchQueries).toContain("\"-openapi\" in:name user:somecorp is:public") + expect(searchQueries).toContain("\"-openapi\" in:name user:techsystems is:public") +}) diff --git a/__test__/projects/ProjectConfigParser.test.ts b/__test__/projects/ProjectConfigParser.test.ts index 4cb0ee0e..4b2256fb 100644 --- a/__test__/projects/ProjectConfigParser.test.ts +++ b/__test__/projects/ProjectConfigParser.test.ts @@ -1,4 +1,4 @@ -import { ProjectConfigParser } from "../../src/features/projects/domain" +import { ProjectConfigParser } from "@/features/projects/domain" test("It parses an empty string", async () => { const sut = new ProjectConfigParser() diff --git a/__test__/projects/getSelection.test.ts b/__test__/projects/getProjectSelectionFromPath.test.ts similarity index 93% rename from __test__/projects/getSelection.test.ts rename to __test__/projects/getProjectSelectionFromPath.test.ts index a5a544ca..7991fb1d 100644 --- a/__test__/projects/getSelection.test.ts +++ b/__test__/projects/getProjectSelectionFromPath.test.ts @@ -1,7 +1,7 @@ -import { getSelection } from "../../src/features/projects/domain" +import { getProjectSelectionFromPath } from "@/features/projects/domain" test("It selects the first project when there is only one project and path is empty", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "", projects: [{ id: "foo", @@ -28,7 +28,7 @@ test("It selects the first project when there is only one project and path is em }) test("It selects the first version and specification of the specified project", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "/acme/bar", projects: [{ id: "foo", @@ -71,7 +71,7 @@ test("It selects the first version and specification of the specified project", }) test("It selects the first specification of the specified project and version", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "/acme/bar/baz2", projects: [{ id: "foo", @@ -110,7 +110,7 @@ test("It selects the first specification of the specified project and version", }) test("It selects the specification of the specified version", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "/acme/bar/baz2", projects: [{ id: "foo", @@ -153,7 +153,7 @@ test("It selects the specification of the specified version", () => { }) test("It selects the specified project, version, and specification", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "/acme/bar/baz2/hello2", projects: [{ id: "foo", @@ -196,7 +196,7 @@ test("It selects the specified project, version, and specification", () => { }) test("It returns a undefined project, version, and specification when the selected project cannot be found", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "/acme/foo", projects: [{ id: "bar", @@ -213,7 +213,7 @@ test("It returns a undefined project, version, and specification when the select }) test("It returns a undefined version and specification when the selected version cannot be found", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "/acme/foo/bar", projects: [{ id: "foo", @@ -236,7 +236,7 @@ test("It returns a undefined version and specification when the selected version }) test("It returns a undefined specification when the selected specification cannot be found", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "/acme/foo/bar/baz", projects: [{ id: "foo", @@ -263,7 +263,7 @@ test("It returns a undefined specification when the selected specification canno }) test("It moves specification ID to version ID if needed", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "/acme/foo/bar/baz", projects: [{ id: "foo", diff --git a/__test__/projects/projectNavigator.test.ts b/__test__/projects/projectNavigator.test.ts index 4cc0cb6a..d1d71825 100644 --- a/__test__/projects/projectNavigator.test.ts +++ b/__test__/projects/projectNavigator.test.ts @@ -1,4 +1,4 @@ -import { ProjectNavigator } from "../../src/features/projects/domain" +import { ProjectNavigator } from "@/features/projects/domain" test("It navigates to the correct path", async () => { let pushedPath: string | undefined diff --git a/__test__/projects/updateWindowTitle.test.ts b/__test__/projects/updateWindowTitle.test.ts index 5521f406..86509b38 100644 --- a/__test__/projects/updateWindowTitle.test.ts +++ b/__test__/projects/updateWindowTitle.test.ts @@ -1,4 +1,4 @@ -import { updateWindowTitle } from "../../src/features/projects/domain" +import { updateWindowTitle } from "@/features/projects/domain" test("It uses default title when there is no selection", async () => { const store: { title: string } = { title: "" } diff --git a/__test__/utils/splitOwnerAndRepository.test.ts b/__test__/utils/splitOwnerAndRepository.test.ts new file mode 100644 index 00000000..d4c162a6 --- /dev/null +++ b/__test__/utils/splitOwnerAndRepository.test.ts @@ -0,0 +1,26 @@ +import { splitOwnerAndRepository } from "@/common" + +test("It returns undefined when string includes no slash", async () => { + const result = splitOwnerAndRepository("foo") + expect(result).toBeUndefined() +}) + +test("It returns undefined when repository is empty", async () => { + const result = splitOwnerAndRepository("foo/") + expect(result).toBeUndefined() +}) + +test("It returns undefined when owner is empty", async () => { + const result = splitOwnerAndRepository("/foo") + expect(result).toBeUndefined() +}) + +test("It splits owner and repository", async () => { + const result = splitOwnerAndRepository("acme/foo") + expect(result).toEqual({ owner: "acme", repository: "foo" }) +}) + +test("It splits owner and repository for repository name containing a slash", async () => { + const result = splitOwnerAndRepository("acme/foo/bar") + expect(result).toEqual({ owner: "acme", repository: "foo/bar" }) +}) diff --git a/package-lock.json b/package-lock.json index bf0d253e..3ec07985 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,11 @@ "@emotion/react": "^11.13.0", "@emotion/styled": "^11.13.0", "@fortawesome/fontawesome-svg-core": "^6.6.0", + "@fortawesome/free-brands-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.2", - "@mui/icons-material": "^5.16.4", - "@mui/material": "^5.16.4", + "@mui/icons-material": "^5.16.5", + "@mui/material": "^5.16.5", "@octokit/auth-app": "^7.1.0", "@octokit/core": "^6.1.2", "@octokit/webhooks": "^13.3.0", @@ -26,7 +27,7 @@ "ioredis": "^5.4.1", "mobx": "^6.13.1", "next": "^14.2.5", - "next-auth": "^5.0.0-beta.19", + "next-auth": "^5.0.0-beta.20", "nodemailer": "^6.9.14", "npm": "^10.8.2", "octokit": "^4.0.2", @@ -39,13 +40,13 @@ "swagger-ui-react": "^5.17.14", "swr": "^2.2.5", "usehooks-ts": "^3.1.0", - "yaml": "^2.4.5", + "yaml": "^2.5.0", "zod": "^3.23.8" }, "devDependencies": { - "@auth/pg-adapter": "^1.4.1", + "@auth/pg-adapter": "^1.4.2", "@types/jest": "^29.5.12", - "@types/node": "^20.14.11", + "@types/node": "^22.0.0", "@types/nodemailer": "^6.4.15", "@types/pg": "^8.11.6", "@types/react": "^18.3.3", @@ -57,8 +58,8 @@ "eslint": "^8.57.0", "eslint-config-next": "^14.2.5", "pg": "^8.12.0", - "postcss": "^8.4.39", - "tailwindcss": "^3.4.6", + "postcss": "^8.4.40", + "tailwindcss": "^3.4.7", "ts-jest": "^29.2.3", "typescript": "^5.5.4" }, @@ -94,10 +95,9 @@ } }, "node_modules/@auth/core": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.34.1.tgz", - "integrity": "sha512-tuYU2VIbI8rFbkSwP710LmybB2FXJsPN7j3sjRVfN9SXVQBK2ej6LdewQaofpBGp4Mk+cC2UeiGNH0or4tgaeA==", - "dev": true, + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.34.2.tgz", + "integrity": "sha512-KywHKRgLiF3l7PLyL73fjLSIBe1YNcA6sMeew4yMP6cfCWGXZrkkXd32AjRi1hlJ9nvovUBGZHvbn+LijO6ZeQ==", "dependencies": { "@panva/hkdf": "^1.1.1", "@types/cookie": "0.6.0", @@ -125,12 +125,12 @@ } }, "node_modules/@auth/pg-adapter": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@auth/pg-adapter/-/pg-adapter-1.4.1.tgz", - "integrity": "sha512-pkZr5NFeju7ONHm/l0VnibjqGFcshhk34HyuglPZVWJba6dXyK7WGkKFV4vAbxKKkfkn3paAfTrjjkWgRNL87Q==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@auth/pg-adapter/-/pg-adapter-1.4.2.tgz", + "integrity": "sha512-L7IAHv6yZN695OFKro0cbny7Nq5XwAF7R84YglQUcNkVFn3IffMWFVFGT6T+WM9Shdc3e1whceqcBJ1+A8WW4g==", "dev": true, "dependencies": { - "@auth/core": "0.34.1" + "@auth/core": "0.34.2" }, "peerDependencies": { "pg": "^8" @@ -964,6 +964,17 @@ "node": ">=6" } }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz", + "integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.6.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@fortawesome/free-solid-svg-icons": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", @@ -2315,18 +2326,18 @@ "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.4.tgz", - "integrity": "sha512-rNdHXhclwjEZnK+//3SR43YRx0VtjdHnUFhMSGYmAMJve+KiwEja/41EYh8V3pZKqF2geKyfcFUenTfDTYUR4w==", + "version": "5.16.5", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.5.tgz", + "integrity": "sha512-ziFn1oPm6VjvHQcdGcAO+fXvOQEgieIj0BuSqcltFU+JXIxjPdVYNTdn2HU7/Ak5Gabk6k2u7+9PV7oZ6JT5sA==", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" } }, "node_modules/@mui/icons-material": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.4.tgz", - "integrity": "sha512-j9/CWctv6TH6Dou2uR2EH7UOgu79CW/YcozxCYVLJ7l03pCsiOlJ5sBArnWJxJ+nGkFwyL/1d1k8JEPMDR125A==", + "version": "5.16.5", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.5.tgz", + "integrity": "sha512-bn88xxU/J9UV0s6+eutq7o3TTOrOlbCX+KshFb8kxgIxJZZfYz3JbAXVMivvoMF4Md6jCVUzM9HEkf4Ajab4tw==", "dependencies": { "@babel/runtime": "^7.23.9" }, @@ -2349,15 +2360,15 @@ } }, "node_modules/@mui/material": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.4.tgz", - "integrity": "sha512-dBnh3/zRYgEVIS3OE4oTbujse3gifA0qLMmuUk13ywsDCbngJsdgwW5LuYeiT5pfA8PGPGSqM7mxNytYXgiMCw==", + "version": "5.16.5", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.5.tgz", + "integrity": "sha512-eQrjjg4JeczXvh/+8yvJkxWIiKNHVptB/AqpsKfZBWp5mUD5U3VsjODMuUl1K2BSq0omV3CiO/mQmWSSMKSmaA==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/core-downloads-tracker": "^5.16.4", - "@mui/system": "^5.16.4", + "@mui/core-downloads-tracker": "^5.16.5", + "@mui/system": "^5.16.5", "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.4", + "@mui/utils": "^5.16.5", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.10", "clsx": "^2.1.0", @@ -2393,12 +2404,12 @@ } }, "node_modules/@mui/private-theming": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.4.tgz", - "integrity": "sha512-ZsAm8cq31SJ37SVWLRlu02v9SRthxnfQofaiv14L5Bht51B0dz6yQEoVU/V8UduZDCCIrWkBHuReVfKhE/UuXA==", + "version": "5.16.5", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.5.tgz", + "integrity": "sha512-CSLg0YkpDqg0aXOxtjo3oTMd3XWMxvNb5d0v4AYVqwOltU8q6GvnZjhWyCLjGSCrcgfwm6/VDjaKLPlR14wxIA==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.16.4", + "@mui/utils": "^5.16.5", "prop-types": "^15.8.1" }, "engines": { @@ -2450,15 +2461,15 @@ } }, "node_modules/@mui/system": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.4.tgz", - "integrity": "sha512-ET1Ujl2/8hbsD611/mqUuNArMCGv/fIWO/f8B3ZqF5iyPHM2aS74vhTNyjytncc4i6dYwGxNk+tLa7GwjNS0/w==", + "version": "5.16.5", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.5.tgz", + "integrity": "sha512-uzIUGdrWddUx1HPxW4+B2o4vpgKyRxGe/8BxbfXVDPNPHX75c782TseoCnR/VyfnZJfqX87GcxDmnZEE1c031g==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.16.4", + "@mui/private-theming": "^5.16.5", "@mui/styled-engine": "^5.16.4", "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.4", + "@mui/utils": "^5.16.5", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -2502,11 +2513,12 @@ } }, "node_modules/@mui/utils": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.4.tgz", - "integrity": "sha512-nlppYwq10TBIFqp7qxY0SvbACOXeOjeVL3pOcDsK0FT8XjrEXh9/+lkg8AEIzD16z7YfiJDQjaJG2OLkE7BxNg==", + "version": "5.16.5", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.5.tgz", + "integrity": "sha512-CwhcA9y44XwK7k2joL3Y29mRUnoBt+gOZZdGyw7YihbEwEErJYBtDwbZwVgH68zAljGe/b+Kd5bzfl63Gi3R2A==", "dependencies": { "@babel/runtime": "^7.23.9", + "@mui/types": "^7.2.15", "@types/prop-types": "^15.7.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -4818,11 +4830,11 @@ "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==" }, "node_modules/@types/node": { - "version": "20.14.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", - "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.0.tgz", + "integrity": "sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.11.1" } }, "node_modules/@types/nodemailer": { @@ -13001,16 +13013,16 @@ } }, "node_modules/next-auth": { - "version": "5.0.0-beta.19", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.19.tgz", - "integrity": "sha512-YHu1igcAxZPh8ZB7GIM93dqgY6gcAzq66FOhQFheAdOx1raxNcApt05nNyNCSB6NegSiyJ4XOPsaNow4pfDmsg==", + "version": "5.0.0-beta.20", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.20.tgz", + "integrity": "sha512-+48SjV9k9AtUU3JbEIa4PXNjKIewfFjVGL7Xs2RKkuQ5QqegDNIQiIG8sLk6/qo7RTScQYIGKgeQ5IuQRtrTQg==", "dependencies": { - "@auth/core": "0.32.0" + "@auth/core": "0.34.2" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", - "next": "^14 || ^15.0.0-0", + "next": "^14.0.0-0 || ^15.0.0-0", "nodemailer": "^6.6.5", "react": "^18.2.0 || ^19.0.0-0" }, @@ -13026,36 +13038,6 @@ } } }, - "node_modules/next-auth/node_modules/@auth/core": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.32.0.tgz", - "integrity": "sha512-3+ssTScBd+1fd0/fscAyQN1tSygXzuhysuVVzB942ggU4mdfiTbv36P0ccVnExKWYJKvu3E2r3/zxXCCAmTOrg==", - "dependencies": { - "@panva/hkdf": "^1.1.1", - "@types/cookie": "0.6.0", - "cookie": "0.6.0", - "jose": "^5.1.3", - "oauth4webapi": "^2.9.0", - "preact": "10.11.3", - "preact-render-to-string": "5.2.3" - }, - "peerDependencies": { - "@simplewebauthn/browser": "^9.0.1", - "@simplewebauthn/server": "^9.0.2", - "nodemailer": "^6.8.0" - }, - "peerDependenciesMeta": { - "@simplewebauthn/browser": { - "optional": true - }, - "@simplewebauthn/server": { - "optional": true - }, - "nodemailer": { - "optional": true - } - } - }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -16395,9 +16377,9 @@ } }, "node_modules/postcss": { - "version": "8.4.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "version": "8.4.40", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", + "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", "dev": true, "funding": [ { @@ -18938,9 +18920,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.6", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz", - "integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==", + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz", + "integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==", "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -19459,9 +19441,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.11.1.tgz", + "integrity": "sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==" }, "node_modules/unified": { "version": "9.2.2", @@ -20226,9 +20208,9 @@ "peer": true }, "node_modules/yaml": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", - "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", "bin": { "yaml": "bin.mjs" }, diff --git a/package.json b/package.json index 50ec2a4a..24eea081 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,11 @@ "@emotion/react": "^11.13.0", "@emotion/styled": "^11.13.0", "@fortawesome/fontawesome-svg-core": "^6.6.0", + "@fortawesome/free-brands-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.2", - "@mui/icons-material": "^5.16.4", - "@mui/material": "^5.16.4", + "@mui/icons-material": "^5.16.5", + "@mui/material": "^5.16.5", "@octokit/auth-app": "^7.1.0", "@octokit/core": "^6.1.2", "@octokit/webhooks": "^13.3.0", @@ -33,7 +34,7 @@ "ioredis": "^5.4.1", "mobx": "^6.13.1", "next": "^14.2.5", - "next-auth": "^5.0.0-beta.19", + "next-auth": "^5.0.0-beta.20", "nodemailer": "^6.9.14", "npm": "^10.8.2", "octokit": "^4.0.2", @@ -46,13 +47,13 @@ "swagger-ui-react": "^5.17.14", "swr": "^2.2.5", "usehooks-ts": "^3.1.0", - "yaml": "^2.4.5", + "yaml": "^2.5.0", "zod": "^3.23.8" }, "devDependencies": { - "@auth/pg-adapter": "^1.4.1", + "@auth/pg-adapter": "^1.4.2", "@types/jest": "^29.5.12", - "@types/node": "^20.14.11", + "@types/node": "^22.0.0", "@types/nodemailer": "^6.4.15", "@types/pg": "^8.11.6", "@types/react": "^18.3.3", @@ -64,8 +65,8 @@ "eslint": "^8.57.0", "eslint-config-next": "^14.2.5", "pg": "^8.12.0", - "postcss": "^8.4.39", - "tailwindcss": "^3.4.6", + "postcss": "^8.4.40", + "tailwindcss": "^3.4.7", "ts-jest": "^29.2.3", "typescript": "^5.5.4" } diff --git a/src/app/(authed)/(home)/[[...slug]]/layout.tsx b/src/app/(authed)/(home)/[[...slug]]/layout.tsx new file mode 100644 index 00000000..36fb09c1 --- /dev/null +++ b/src/app/(authed)/(home)/[[...slug]]/layout.tsx @@ -0,0 +1,23 @@ +"use client" + +import SecondarySplitHeader from "@/features/sidebar/view/SecondarySplitHeader" +import TrailingToolbarItem from "@/features/projects/view/toolbar/TrailingToolbarItem" +import MobileToolbar from "@/features/projects/view/toolbar/MobileToolbar" +import { useProjectSelection } from "@/features/projects/data" + +export default function Page({ children }: { children: React.ReactNode }) { + const { project } = useProjectSelection() + if (!project) { + return <> + } + return ( + <> + > + + +
+ {children} +
+ + ) +} \ No newline at end of file diff --git a/src/app/(authed)/(home)/[[...slug]]/page.tsx b/src/app/(authed)/(home)/[[...slug]]/page.tsx new file mode 100644 index 00000000..e70225d5 --- /dev/null +++ b/src/app/(authed)/(home)/[[...slug]]/page.tsx @@ -0,0 +1,43 @@ +"use client" + +import { useContext, useEffect } from "react" +import { ProjectsContainerContext } from "@/common" +import DelayedLoadingIndicator from "@/common/ui/DelayedLoadingIndicator" +import ErrorMessage from "@/common/ui/ErrorMessage" +import { updateWindowTitle } from "@/features/projects/domain" +import { useProjectSelection } from "@/features/projects/data" +import Documentation from "@/features/projects/view/Documentation" + +export default function Page() { + const { error, isLoading } = useContext(ProjectsContainerContext) + const { project, version, specification, navigateToSelectionIfNeeded } = useProjectSelection() + // Ensure the URL reflects the current selection of project, version, and specification. + useEffect(() => { + navigateToSelectionIfNeeded() + }, [project, version, specification, navigateToSelectionIfNeeded]) + // Update the window title to match selected project. + const siteName = process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE || "" + useEffect(() => { + updateWindowTitle({ + storage: document, + defaultTitle: siteName, + project, + version, + specification + }) + }, [siteName, project, version, specification]) + if (project && version && specification) { + return + } else if (project && !version) { + return + } else if (project && !specification) { + return + } else if (isLoading) { + return + } else if (error) { + return + } else { + // No project is selected so we will not show anything. + return <> + } +} diff --git a/src/app/(authed)/(home)/layout.tsx b/src/app/(authed)/(home)/layout.tsx new file mode 100644 index 00000000..49b27871 --- /dev/null +++ b/src/app/(authed)/(home)/layout.tsx @@ -0,0 +1,36 @@ +"use client" + +import { useContext } from "react" +import { SplitView } from "@/features/sidebar/view" +import { useProjects, useProjectSelection } from "@/features/projects/data" +import { + ProjectsContainerContext, + ServerSideCachedProjectsContext +} from "@/common" + +export default function Layout({ children }: { children: React.ReactNode }) { + const { projects, error, isLoading } = useProjects() + // Update projects provided to child components, using cached projects from the server if needed. + const serverSideCachedProjects = useContext(ServerSideCachedProjectsContext) + const newProjectsContainer = { projects, error, isLoading } + if (isLoading && serverSideCachedProjects) { + newProjectsContainer.isLoading = false + newProjectsContainer.projects = serverSideCachedProjects + } + return ( + + + {children} + + + ) +} + +const SplitViewWrapper = ({ children }: { children: React.ReactNode }) => { + const { project } = useProjectSelection() + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/src/app/(authed)/(home)/new/page.tsx b/src/app/(authed)/(home)/new/page.tsx new file mode 100644 index 00000000..bdbbd356 --- /dev/null +++ b/src/app/(authed)/(home)/new/page.tsx @@ -0,0 +1,56 @@ +import Link from "next/link" +import { env, splitOwnerAndRepository } from "@/common" + +const Page = () => { + const repositoryNameSuffix = env.getOrThrow("REPOSITORY_NAME_SUFFIX") + const templateName = env.get("NEW_PROJECT_TEMPLATE_REPOSITORY") + const projectName = "Nordisk Film" + const suffixedRepositoryName = makeFullRepositoryName({ + name: projectName, + suffix: repositoryNameSuffix + }) + const newGitHubRepositoryLink = makeNewGitHubRepositoryLink({ + templateName, + repositoryName: suffixedRepositoryName, + description: `Contains OpenAPI specifications for ${projectName}` + }) + return ( + + {newGitHubRepositoryLink} + + ) +} + +export default Page + +function makeFullRepositoryName({ name, suffix }: { name: string, suffix: string }) { + const safeRepositoryName = name + .trim() + .toLowerCase() + .replace(/[^a-z0-9-]+/g, "") + .replace(/\s+/g, "-") + return `${safeRepositoryName}${suffix}` +} + +function makeNewGitHubRepositoryLink({ + templateName, + repositoryName, + description +}: { + templateName?: string, + repositoryName: string, + description: string +}) { + let url = `https://github.com/new` + + `?name=${encodeURIComponent(repositoryName)}` + + `&description=${encodeURIComponent(description)}` + + `&visibility=private` + if (templateName) { + const templateRepository = splitOwnerAndRepository(templateName) + if (templateRepository) { + url += `&template_owner=${encodeURIComponent(templateRepository.owner)}` + url += `&template_name=${encodeURIComponent(templateRepository.repository)}` + } + } + return url +} diff --git a/src/app/(authed)/layout.tsx b/src/app/(authed)/layout.tsx new file mode 100644 index 00000000..bc977cf5 --- /dev/null +++ b/src/app/(authed)/layout.tsx @@ -0,0 +1,25 @@ +import { redirect } from "next/navigation" +import { SessionProvider } from "next-auth/react" +import { session, projectRepository } from "@/composition" +import ErrorHandler from "@/common/ui/ErrorHandler" +import SessionBarrier from "@/features/auth/view/SessionBarrier" +import ServerSideCachedProjectsProvider from "@/features/projects/view/ServerSideCachedProjectsProvider" + +export default async function Layout({ children }: { children: React.ReactNode }) { + const isAuthenticated = await session.getIsAuthenticated() + if (!isAuthenticated) { + return redirect("/api/auth/signin") + } + const projects = await projectRepository.get() + return ( + + + + + {children} + + + + + ) +} \ No newline at end of file diff --git a/src/app/[[...slug]]/page.tsx b/src/app/[[...slug]]/page.tsx deleted file mode 100644 index 092f7f40..00000000 --- a/src/app/[[...slug]]/page.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { redirect } from "next/navigation" -import { SessionProvider } from "next-auth/react" -import { session, projectRepository } from "@/composition" -import ErrorHandler from "@/common/errors/client/ErrorHandler" -import SessionBarrier from "@/features/auth/view/SessionBarrier" -import ProjectsPage from "@/features/projects/view/ProjectsPage" - -type PageParams = { slug: string | string[] } - -export default async function Page({ params }: { params: PageParams }) { - const isAuthenticated = await session.getIsAuthenticated() - if (!isAuthenticated) { - return redirect("/api/auth/signin") - } - return ( - - - - - - - - ) -} - -function getPath(slug: string | string[] | undefined) { - if (slug === undefined) { - return "/" - } else if (typeof slug === "string") { - return "/" + slug - } else { - return slug.reduce((e, acc) => `${e}/${acc}`, "") - } -} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 1d38a1d9..85cc4d25 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,3 +1,3 @@ -import { auth } from "@/composition" +import { authHandlers } from "@/composition" -export const { GET, POST } = auth.handlers +export const { GET, POST } = authHandlers diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx new file mode 100644 index 00000000..97e53ae0 --- /dev/null +++ b/src/app/auth/signin/page.tsx @@ -0,0 +1,127 @@ +import Image from "next/image" +import Link from "next/link" +import { Box, Button, Stack, Typography } from "@mui/material" +import { signIn } from "@/composition" +import { env } from "@/common" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { faGithub } from "@fortawesome/free-brands-svg-icons" +import SignInTexts from "@/features/auth/view/SignInTexts" + +const SITE_NAME = env.getOrThrow("NEXT_PUBLIC_SHAPE_DOCS_TITLE") +const HELP_URL = env.get("NEXT_PUBLIC_SHAPE_DOCS_HELP_URL") + +export default async function SignInPage() { + return ( + + + + + ) +} + +const InfoColumn = () => { + return ( + + + + ) +} + +const SignInColumn = () => { + const title = `Get started with ${SITE_NAME}` + return ( + + + + + {`${SITE_NAME} + + + {title} + + + {title} + + + + + +